Уважаемые жители 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)

Комментарии (44)


  1. lorc
    00.00.0000 00:00
    +1

    Интересно. Но есть несколько замечаний:

    • Я вижу что генерируете барьеры для процессора, но почему-то забыли про барьер для компилятора. Он же может и переставить инструкции записи/чтения местами. Обычно ставят что-то типа asm volatile("dsb st;" : : : "memory");

    • Компилятор не знает что это доступ к Device Memory, поэтому чисто теоретически может сгенерировать какую-то дичь, типа SIMD Load/Store, что вызовет интересные спецэффекты. Но гораздо чаще он будет генерировать инструкцию со смещением (типа ldr x0, [x1], #offset), что потом вызовет проблемы, если у вас есть гипервизор, который будет пытаться эмулировать MMIO. Поэтому почти везде используется явно закодированный вариант LDR/STR. Как это натянуть на C++ - я даже не представляю.

    Естественно, это все относится к ARMv8-A и ARMv7-A, где есть MMU. На всяких M profile где есть в лучшем случае только PMU можно и не парится.


    1. EvgenySbl Автор
      00.00.0000 00:00
      +1

      Спасибо, я изучу данный вопрос подробнее.

      Однако, еще ни разу не встречал чтобы компилятор переставил последовательность инструкций, если переменная явно объявлена как volatile. В коде, практически везде явно указано *(reinterpret_cast<volatile type* const>(address)) = xxx. Или наоборот чтение. В таком случае, это всегда приводит к инструкции str rx, [ry, #offset], смещение опционально. Если у Вас имеется код, который вызвал подобные альтернативные обращения, с удовольствием посмотрю.

      Еще, можно сам метод объявить как volatile (между закрывающией скобкой и открывающией фигурной скобкой), компилятор не будет его оптимизировать. Я так делать не стал, поскольку ряд инструкций не схлопнуться в процессе оптимизации, и в ряде случаев вместо одного "orr" и одного "and" получтся несколько таких инструкций.

      Ассемблерные инструкции, тоже можно прописать с параметрами, и явно указать str. Однако, это не всегда удобно. Полагаю, для барьеров тоже есть intrinsic-и

      Касательно альтернативных инструкций. Нужно подумать. Я знаю что ряд архитетур использует не просто пространство памяти. Некоторые, в своей адресации используют отдельный IO SPACE. Про адресацию регистров сопроцессора, вообще молчу. Работал с ARM, ARM CORTEX A, ARM CORTEX M, MIPS, PowerPC, AVR, PIC, очень давно с x86. Но последнее время в основном ARM, ARM-Cortex.

      Касательно MMU. Как может повлиять само MMU? Если там есть гипервизор, он просто сгенерирует страничный сбой, вызовется обработчик, который сформирует альтернативное обращение. Или не вызовется ничего, если память замеплена и установлены соответствующие биты. В моем случае, я перенес все вектора обработчиков: USR, SVC, MON, HYP, в обоих режимах с NX = 0 и 1.


      1. fk0
        00.00.0000 00:00
        +1

        volatile не является барьером. Точка. Надоело спорить уже, на rsdn 10 раз писал. Он является барьером у микрософта, в MSVC, но это специфика только микрософтовского компилятора.

        Компилятор имеет право перебросить все остальные обращения к памяти перед/после обращения к volatile. Это стоит всегда помнить. По сути volatile -- это такой relaxed atomic, причём определённый только для ограниченного множества типов (int, char, long...) Для какого-нибудь массива char[2] или struct { char x, y; } он может не сработать (обращение будет не атомарное).


        1. EvgenySbl Автор
          00.00.0000 00:00

          Да никто и не говорит что volatile, это барьер.

          Чтобы было меньше вопросов и больше понимания как это работает. Процессор, может переупорядочивать инструкции. Для решения этого вопроса, существуют барьерные инструкции из разряда. Я чаще пишу код под arm, по этому буду говрить о инструкциях arm. Для того чтобы к определенному моменту все предыдущие инструкции завершили работу, есть барьерная инструкция isb. Далее, процессор может переупорядочивать инструкции доступа к шине. Т.е. в нашем случае memory mapped регистрам. Для решения этого вопроса, существуют барьерные инструкции из разряда dsb. Еще, процессор не сразу пишет данные на шину. Существует кэш. И если он не WriteThrow, то, его тоже нужно сбрасывать. Однако, область памяти работающая с регистрами обычно объявляется не кэшируемой. Более того кэшей может быть несколько. Если говорить о ARM, то их обычно два L1 и L2. L1 как правило раздельный для инструкций и данных, а L2 чаще общий. Кроме кэша, есть еще один вопрос. Данные, поступающие на шину, не пишутся в нее сразу из кэша. Существует, так называемый WriteBuffer.

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

          	inline void preRead(void) {
          		asm("dsb ld");
          	}
          
          	inline void postWrite(void){
          		asm("dsb st");
          	}

          Более того весь сыр-бор, был в основном как раз таки из-за этих функций.

          // Тут видно, что в код не включается инструкция чтения, 
          // и соотвестсвующая ей барьерная инструкция,
          // в случае если переписывается весь регистр целиком,
          // а не отдельные его поля.
            template <typename... Fields>
          		inline void Write( const typename Fields::Type... args ){
          			constexpr const typename Reg::Value::Type ReservedMask = getRegReservedMaskInt< Reg >();
          			constexpr const typename Reg::Value::Type ConcatMask = getRegMaskInt< Reg, Fields...>() | ReservedMask;
          			if constexpr ( ConcatMask == Reg::Value::Description::getBitMask() ) {
          				typename Reg::Value::Type regValue = getRegValueInt<Reg, Fields...>( args... );
          				*reinterpret_cast<volatile typename Reg::Value::Type* const>(_address) = regValue;
          				postWrite();
          			} else {
          				preRead();
          				typename Reg::Value::Type regValue = *reinterpret_cast<volatile typename Reg::Value::Type* const>(_address);
          				regValue &= ~( ConcatMask );
          				regValue |= getRegValueInt<Reg, Fields...>( args... );
          				*reinterpret_cast<volatile typename Reg::Value::Type* const>(_address) = regValue;
          				postWrite();
          			}
          		}


          1. lorc
            00.00.0000 00:00

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

            REG1=0x14;
            REG3=0x12;
            REG2=0x11;
            

            То компилятор имеет право переставить их местами:

            REG1=0x14;
            REG2=0x11;
            REG3=0x12;
            

            Потому что он не видит зависимостей межу этими записями. А то что после этого у вас перестанет правильно работать периферия - это не его проблема. Поэтому везде (я видел это linux, u-boot, xen, optee, zephyr) всегда ставят костыль, который обломает компилятору
            проверку зависимостей между инструкциями. Минимальный вариант - это asm volatile (:::"memory").


      1. lorc
        00.00.0000 00:00
        +1

        Однако, еще ни разу не встречал чтобы компилятор переставил последовательность инструкций, если переменная явно объявлена как volatile.

        Ну просто есть стандарт. Насколько я понял, он не гарантирует барьеры. Вот тут есть интересное обсуждение: https://stackoverflow.com/questions/26307071/does-the-c-volatile-keyword-introduce-a-memory-fence

        В таком случае, это всегда приводит к инструкции str rx, [ry, #offset]

        Это приведет к проблемам с виртуализацией. Когда гипервизор трапнет запись в регистр используя вот такую инструкцию, он не сможет разобрать Instruction Syndrome чтобы правильно сэмулировать запись.

        Я так делать не стал, поскольку ряд инструкций не схлопнуться в процессе оптимизации, и в ряде случаев вместо одного "orr" и одного "and" получтся несколько таких инструкций.

        Главное, чтобы не генерировал несколько STR/LDR к регистрам.

        Касательно MMU. Как может повлиять само MMU?

        Обычная память мапится c атрибутом Normal Memory. Это разрешает кеширование, склеивание несколько LDR/STR в одну большую транзакцию, перестановку операций обращения к памяти и еще много интересных штук.

        Memory Mapped IO (MMIO, там, где лежат регистры периферии) регионы должны мапится с атрибутом Device Memory. Это сильно ограничивает процессор в том, что он может делать с транзакциями к таким регионам.

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

        Это если он сможет разобрать синдром инструкции. Либо, как альтернатива - дизассемблировать на ходу инструкцию, которая вызвала сбой. Но это дорого и этого никто не делает. Гипервизор просто смотрит в HSR и все.


        1. EvgenySbl Автор
          00.00.0000 00:00

          Это приведет к проблемам с виртуализацией. Когда гипервизор трапнет запись в регистр используя вот такую инструкцию, он не сможет разобрать Instruction Syndrome чтобы правильно сэмулировать запись.

          Вопрос в том, что и насколько глубоко Вы планируете виртуализовать. Если виртуальная машина Ваша и код гипревизора Ваш. Я бы подумал над тем, чтобы основную часть дрйверов перенести под гипервизор. Со стороны ОС можно осуществлять системные вызовы в гипервизор или монитор режим. Для этого в ARM есть SMC, SVC, HYP - вызовы. Это значительно сократит масшатаб трагедии. Если не ошибаюсь SVC будет использоваться прикладными процессами для вызова ядерных функций ОС, а дальше ОС, при небходиомсти сделает системный вызов к нижележащему драйверу паравиртуализации. Если же говорить о единичной записи в простые порты, то, по идее ничего сложного нет в том, чтобы разобрать инструкции. Да,- это дорого с точки зрения накладных расходов, но это единичные инструкции. Т.к. вся основная работа с IO останется на уровне дарйвера гипервизора. Да, я понимаю, что будет смена режима работы процессора, но лучше если она будет на транзакцию, чем на регистр. С другой стороны, если Вы захотите именно подсмотреть что происходит в неизвестной системе, почему бы не занятся определением синдрома инструкции? Вам же не нужно трапаться на всех страницах памяти, а только на нужных. Или Вы хотите полную трассировку кода сделать?


          1. lorc
            00.00.0000 00:00
            +1

            Ну я же просто так написал про все это, а исходя из собственного опыта. Я вообще занимаюсь активной разработкой и поддержкой Xen на aarch64.

            Если виртуальная машина Ваша и код гипревизора Ваш. Я бы подумал над тем, чтобы основную часть дрйверов перенести под гипервизор.

            Собственно, на ARM есть стандарт System Ready, который в том числе предъявляет требования к виртуализации. Например часть обращений к GIC как раз виртуализируются методом перехвата MMIO, чтобы виртуальная машина не парилась паравиртуализацией, а могла использовать стандартный драйвер GIC.

            Во время портирования Zephyr RTOS под Xen на aarch64 я как раз столкнулся с этой проблемой - компилятор генерировал STR/LDR с immediate offset, а Xen не мог захендлить такое обращение к памяти из-за ограничений архитектуры aarch64. Собственно, если хотите, можете глянуть на раздел "ISS encoding for an exception from a Data Abort" в документе "ARM DDI 0487I.a" Пришлось делать патч на зефир.

            Да,- это дорого с точки зрения накладных расходов, но это единичные инструкции.

            Во-первых это ОЧЕНЬ дорого - чтобы дизассемблировать инструкцию, надо сначала узнать что это за инструкция, а для этого надо сначала замапить страницу с кодом ядра виртуальной машины в адресное пространство гипервизора. Работа с MMU - это довольно дорого. Да, можно придумать какой-то кеш страниц и т.д, но этого никто не делает. Плюс, есть куча вариантов LDR/STR и эмулировать весь этот зверинец со всеми их побочными эффектами - то еще удовольствие. А учитывая, что прерывания приходят постоянно, то например эмуляция GIC - это далеко не "единичные" инструкции.


            1. EvgenySbl Автор
              00.00.0000 00:00

              Собственно, на ARM есть стандарт System Ready, который в том числе предъявляет требования к виртуализации. Например часть обращений к GIC как раз виртуализируются методом перехвата MMIO, чтобы виртуальная машина не парилась паравиртуализацией, а могла использовать стандартный драйвер GIC.

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

              Во-первых это ОЧЕНЬ дорого - чтобы дизассемблировать инструкцию, надо сначала узнать что это за инструкция, а для этого надо сначала замапить страницу с кодом ядра виртуальной машины в адресное пространство гипервизора. Работа с MMU - это довольно дорого. Да, можно придумать какой-то кеш страниц и т.д, но этого никто не делает. Плюс, есть куча вариантов LDR/STR и эмулировать весь этот зверинец со всеми их побочными эффектами - то еще удовольствие. А учитывая, что прерывания приходят постоянно, то например эмуляция GIC - это далеко не "единичные" инструкции.

              На самом деле, я знаю, что компилятор, очень часто генерирует инструкции обращения к памяти со смещениями. Много кода приходилось писать под ARM. Особенно часто, это можно увидеть при работе с несколькими регистрами одной группы, или структурой. Конечно, гипервизор, мне писать не приходилось, а с MMU, полноценно, я работал лет 15 назад на ARM926, это была VxWorks. Однако, мысль оттрасировать обращения к регистрам есть. Если предположить что нет привязки к временным интервалам в обращениях к регистрам, то можно не обращать внимания на накладные расходы обработки кода. По правде говоря, это условие не всегда справдливо. Зачастую, для разблокировки какой-то переферии, например flash, в процессорах встречаются последовательные обращения, которые не должны прерываться. Если их нет,- то мне повезло. На данный момент, не важно сколько по времени будет грузиться система, если она запустится. Важно, какие обращения и по каким адресам идут. Для начала,- нужна PLL и оперативка. С остальным,- будет проще. Т.е. фактически, я пишу BSP на систему, на которую нет полноценной документации. А та, что есть,- на китайском языке, и содержит лишь описание полей, без функционала и без Errata. С другой стороны, есть работающий код в основном, в виде бинарных файлов (модулей ядра).

              Почему возникло желание оттрасировать? Просто, поставили задачу разработки именно на эту камеру, пообщали неплохие деньги, а с работой было тяжело, кто-то подтапливал, "поливая грязной водой" и отбивал нормальных работодателей. Другой работы небыло именно по этой причине.

              Начал разбираться, в коде, и увидел непонятное условие, которое вызывет инициализацию PLL, и еще ряда переферии. Причем PLL, похоже, инициализируется с применением скрипта (ADDRESS-VALUE-DELAY-FLAGS). А он, при линейном выполенении, вызываться не может. Для этого должно произойти что-то специфическое. Собственно, начал с переноса всех векторов к себе в код и переписывания того что инициализируется, так, чтобы мне было удобно и понятно.


  1. Tuxman
    00.00.0000 00:00
    +6

    Но есть нюанс. Си программисты с большим стажем, особенно system level и embedded, они хоть и пишут на C++, но как-то больше в формате С, и когда им даёшь код как выше, где всё такое сильно template, они отказываются с тобой работать ;-)


    1. BobovorTheCommentBeast
      00.00.0000 00:00
      +7

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


      1. Goron_Dekar
        00.00.0000 00:00

        Однако, если в запаянном девайсе ошибка с портом проскочит, есть шанс спалить что-то. Поэтому читаемость кода при работе с портами крайне важна.

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


        1. EvgenySbl Автор
          00.00.0000 00:00
          +1

          Спасибо за поддержку. Такое бывает редко, при правильно спроектированном железе. Подобные посты, обычно, минусуют заядлые C-шники, не желающие учиться чему-либо. Либо люди, которые "делают работу". Т.е. когда конкурент устраивается в конкурирующую компанию, продолжая неофициально получать деньги в основной, а у конкурента оставляет дыры в безопасности, скрытые закладки, и т.д. А потом увольняется, получает премию в основной компании, и меняет место работы.

          В C-шном коде зачастую спрятать такие вещи проще. Разобраться значительно проще, если есть CodeStyle компании, и если код пишут на строго-типизированных языках. Обычно подобные вещи, максируются под случайные ошибки. Но таких людей можно продавать, и за это тоже платят деньги. Лучше честно.


        1. BobovorTheCommentBeast
          00.00.0000 00:00
          +1

          Я просто не могу представить такой ситуации. Серьезно, проскочить в конечный продукт? В отладочную первую версию - легко, туда и перепутанная земля с питанием проскакивает. Проводками поправят.

          Если бы это было что-то стандартизированное, от производителя или хотя-бы крупный опенсорс проект - ок. Но вопреки истеричным завываниям, о глупых дедах, о твердолобых сишниках -- написать свой костыль-шаблон-ООП-ембеддед велосипед это сорт хеловорлда при начале использования С++ с МК =). Это все делали и в большинстве все ушли от этого. Т.к. как я выше писал -- не понятные цели.

          Тут штук 10 статей про самописные ХАЛ лежат. А суть в том, что если не нравиться родной ХАЛ -- напиши конкретный под текущую цель. KISS и вся фигня, ага. Он будет в 1000 раз проще, однозначнее и читаймее, чем что-то самописное-универсальное. В высоком софте это уже прошли лет так 30+ назад =)

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

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

          Вот после этого кого-то в закладках и работе ради работы не стыдно обвинять? Нахреначить сложноанализируемого кода там, где можно этого не делать или сделать проще, это подложенная свинья пожирнее.


          1. EvgenySbl Автор
            00.00.0000 00:00

            Давайте на чистоту. Никто не хочет писать HAL. Большая часть хочет написать три строчки, и сказать что излелие работат. Берете ардуино, или любой другой продукт вроде малинки для народных масс. И работаете с готовым API. Никаких проблем,- все переносимо, и вроде бы работает. И это правильно с коммерческой точки зрения.

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

            Не так давно, поставили мне задачу, запкстить кое что на заренее выбранной камере. Взял эту камеру на базе HiSilicon, вернее на базе его клона. А на ней залоченная прошивка. Поменял я пароль в shadow файле, чтобы получить доступ к Linux. Получил доступ. Это не хакерство на территории РФ. Нам на официально купленные продукт, нужно доработать ПО, которое сделает продукт совместимым с нашим. Всего то нужно было статически слинковать приложение и запустить. Задача простая. Проще некуда. Но мешает чужой процесс. Мало ли что он там делает? Прибил я чужой процесс, и получил неопределенное поведение, которое заканчивается перезагрузкой камеры. Так быть не должно. Значит на камере защите. Обойти защиту, я вероятно смог бы, но это уже не доработка а взлом, а взломом я не занимаюсь. Начал ковырять дальше. Взял OpenIPC, генератором, сгенерировал бинарь с ресурса, и запустил его. И вроде бы все относительно хорошо. Но, когда я открыл исходинки, то, мягко говоря, был удивлен. Затем я решил их собрать, а они не собираются. В скриптах ошибки. Поправил, - собрал. Сравнил с бинарем. Да, да, - дизассмблируя гидрой, и прописывая комментарии. Оказвается, там есть бинарный блоб, который отвечет за инициализайию регистров, а поптутно, может записать что угодно в какой угодно адрес, что может привести сами знаете к чему. Но этого бинарного блоба, нет в исходных кодах, которые доступны. Ладно, взял скрипт из прошивки, проанализировал код и понял как он запускает этот скрипт. Оперативка, там должна заводиться. Но не заводитбся. Что делать? Для начала, нужно понять что он делает, а для этого нужна понять в какие регистры что пишется. Как это сделать? Я сделал просто. Написал свой модуль регистров, описал регистры, сдампил регистровую область запущенной системы, и начал разбирать как они проинициализированы. Для этого мне нужно было мэпить регитры не только на default-ные адреса, но и на адреса, в которые у меня отображается сдамелнная облать уже в другом компиляторе и на другом процессоре. Да да, - это реверс инжиниринг. Потом, я понял что, возможно, стоит дооптимизировать код, чтобы потом ничего не переписвать, а каждое поле, которое я пишу,- будет понятно. Сижу рассматривю вариант камеры своей собственной на дорогом процессоре, и с собственной схематикой, или вариант на готовой камере, которую нужно пореверсить. Пока образцов других чипов на руках нет,- в свободное время исследую этот. Возможно, у Вас появиться вопрос. А почему бы не использовать те бинари, которые есть? Все просто. Если Вы создавали коммерческий продукт, то знаете что такое этап сопровождения. И конечно, знаете, что такое визит-эффект или эффект генерала. Когда все вроде бы работало, а потом, в опредленный момент вдруг перестало. Представьте что накроется работа десятка тысяч камер по городу, из за того что в библиотете производителя есть маленькая закладка. Для себя,- можно и забить. Для промышленного производства,- нужно или сказать, что этот проект (на этом чипе) можно сделать за N времени с негарантированным результатом и предложить альтернативу, или откзазаться. Это лучше, чем обосраться когда у вас с конвейра сойдет десяток тысяч устройств, а потом, они все разом перестанут работать. Вот для этого и нужен свой HAL.


      1. EvgenySbl Автор
        00.00.0000 00:00
        +2

        Решения есть. Как уже писал, у нас был свой HAL. Хорошо абстрагированный от конкретного железа. Однако, обычно не все так просто. Как опытному системному программисту, мне приходилось разработывать BSP для VxWorks, Windows CE, FreeRTOS, писать драйвера под большую Windows и даже чуть чуть под Linux. Я не хвалюсь, просто широкий кругозор, позволяет охватывать все ньюансы работы на стыке с аппартурой, HAL-ом, OSaL-ом, включая различные SVC, и т.д.

        Нельзя учесть всего. Когда пишешь код, да и в целом что то разрабатываешь, приходится искать компромисы. По этому, разработка это всего вопрос компромиса производительность/потребление/удобство использования/универсальность/стоимость/доступность/взаимо-заменяемость.

        Давайте простой пример. Мне нужен драйвер GPIO. GPIO, потому что на нем проще всего объяснить. С точки зрения абстракции, просто порт. Насторил, записал прочитал. Однако, современные железки имеют кучу настроек. От частоты порта, до выходного сопротивления (скорости нарастания), для того чтобы на длинных линиях звона небыло. Обычно, эти настройки на верхнем уровне решают как то универасально. Либо универсальный HAL, учитывающий все. Либо какой-то ioctl, параметры которого зависят от архитектуры. Это хорошо.

        А что если, мне нужно по-другому. Например. В борьбе за скорость, я хочу запустить таймер, от таймера событием толкнуть DMA, и чтобы этот DMA не привязывался к таймеру, а взял ячейку памяти из кольцевого буфера и переслал в GPIO порт. Или еще дальше. Взять два события от одного или разных таймеров, одно будет обнулять тот же порт через DMA с кольцевым буфером в одну ячейку, а другой будет брать значения из длинного кольцевого буфера, который в это время просчитывается. А еще не следует забывать что DMA не везде универсальный, где то привязан к оборудованию, где то нет, где то имеет chain, где то нет, и т.д. А это не абстрактная задача, я так делал, и HAL STM32, таких фишек не предусматривает. Вот тогда, приходится опускаться на уровень регистров.

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


    1. Rio
      00.00.0000 00:00
      +11

      Как С++ программист, волею случая по-молодости когда-то попавший в system и embedded, тоже поначалу порывался там на плюсах абстракции пилить, С89 в грусть вводил, казался шагом назад. Но вскоре, понабравшись опыта, бросил это дело, потому что пришло понимание, что не нужно оно там совершенно, а нужно чёткость, лаконичность и однозначность, и старый добрый Си в этой нише то что надо.


      1. EvgenySbl Автор
        00.00.0000 00:00
        +2

        Как посмотреть. Старый добрый C,- это не плохо. Более того для него есть ряд систем аналогичных Prefast, для того чтобы проводить анализ кода. Не буд спорить, тут кто к чему привык.

        Просто, мне не нарвится, например, включенные стековые канарейки. Для того чтобы об этом не беспокоится, у C должен быть очень хороший построцессор, с кучей проверок. А в плюсах, многие вещи сразу проверяются (я о типизации). Многие правда нет. И от кривости рук, это не всегда спасает. Указатель или ссылка? Вроде бы одно и тоже, но допустить ошибку во втором случае сложнее. Умные указатели, зачастую лучше чем malloc и free. Просто нужно держать себя в руках, а не использовать их как попало, потому что считаешь что это безопасно.

        Мне нарвится строгая типизация. Мне нравится что можно объявить переменную в любом месте кода. Хотя, по правде говоря, это тоже компромис. Когда я смотрю на это, то вижу сквось код C++ как компилятор декрементирует регистр стека в середине функции, и меня это начинает раздражать. Есть рекомендация, из книги "Совершенный код": Время жизни локальной переменной должно быть минимально. Это немного противоречит оптимальности с точки зрения изменения размера стека, но позволяет допустить меньше ошибок.

        В то же время, с удовольстивем применяю конструкции, позволяющие что то сделать в конструкторе и дестуркторе, причем, возможно даже на уровне работы с аппартурой, а сам алгоритм разместить между скобками. Это красиво, читабельно, скрывает детали реализации. И зачастую, при правильно написанном коде, он может вообще не вызывать ничего, а выполнить inline инструкции. Это как блокировки на семафоре между открывающий и закрывающей скобкой. Блок закончился, блокировака освободилась. То же и с аппаратурой. Можно не думать о деталях, а по эффективности ничего не менятся. Как это сделать в C?

        Вообще говоря, мозг, может абстрагироваться на различных уровнях. Можно думать о том во что скомпилируется нижний уровень Вашего HAL-а, а потом можно абстрагироваться и думать только о обращениях к нему, реализуя алгоритм. То же самое с безопасностью. Когда код покрыт тестами, работает как надо, при передаче аргументов в C++, сложнее совершить ошибку, чем в C. Можно поддерживать более удобо-читаемые конструкции.

        Я за C++, хотя, до 2017 года, послал бы подальше, тех кто предложил писать на плюсх под МК. И сейчас пошлю, если попросят использовать там STL-части c динамическим выделением памяти, т.к. не хочу фрагментировать стек (если нет MMU или мало памяти).

        А вот type_traits, math, куча шаблонных алгоритмов, и т.д. Почему нет?

        Неужели строчки вроде { PA1::Mode::OpenDrain(); PA1::Out::Enable(); PA1::Write(1); } Выглядит хуже, чем записи в виде макросов? А в ассемблере будет то же самое. Писать шаблоны и использовать оттестированные шаблоны, это разные вещи, а еще, это очень удобно, когда у тебя работает "Auto Complete".


        1. yatanai
          00.00.0000 00:00

          Когда-то мне было скучно и я написал библиотеку на шаблонах, которая представляла драйвер устройства как объект. Это была очень тяжёлая либа, по кодстайлу, но работать с ней было одно удовольствие. Пишешь просто mpu.rate = 300; и под капотом оно гоняет байтики по шине.

          И потом я начал изучать сишные проекты с mpuXXXSetRate(mpuXXX_context* ctx, uint32_t rate); и под капотом ещё куча длинных названий...

          Может просто писать много букаф легче чем описывать объекты на С++? - решил я для себя. Меньше рисков накосячить.


    1. EvgenySbl Автор
      00.00.0000 00:00

      Да, зачастую, это так. До 2017 года, сам к таким программистам относился. Потом устроился в Antilatency. Проработал там три года. Первые пол-года плевался на шаблонный ад. Потом привык и понял насколько это круто. А потом, и собственный HAL компании, написанный на плюсах разросся, и охватил многие STM32 и Nordic. Там и USB, и интерфейсы все типовые и интеграция во FreeRTOS. Это здорово, когда ты просто заменяешь репозиторий микроконтроллера в ассоциациях SVN, пересобираешь код, и он запускается уже на другом процессоре и даже другой фирмы, вообще без изменений.

      Касательно тех программистов, кто отказывается работать, вопрос, решается просто. Это же просто регистры. Есть возможность писать сам HAL, как набор функций. А все функции для них extern "C". Пусть пишут на "C", и отлавливают кучу своих багов в runtime, которые можно было отловить в compile-time.


      1. mctMaks
        00.00.0000 00:00

         А потом, и собственный HAL компании, написанный на плюсах разросся, и охватил многие STM32 и Nordic. 

        Разрешите поинтересоваться, в целях повышения образованности, а кто такой будет Иван Федорович Крузенштерн код данного HAL является закрытым? Или все же можно посмотреть некоторые интересные моменты?

        Сейчас по сути занимаюсь тем же, связка Nrf + STM. Задачи однотипные, но сделать универсальным получается только базовые вещи. Плюсом идет то, что плюсы изучаю в процессе. Некоторые вещи получается сделать красиво, некоторые нет. Хотелось бы повысить свои навыки.


        1. EvgenySbl Автор
          00.00.0000 00:00

          Да, код, который приндалежит компании, не должен выходить за пределы компании. Всегда придерживался такого принципа. Если код общий, был написан дома, не в рабочее время,- это мой код, независимо от того принес ли я его в компанию и использовал ли там. Фактически, таким образом, я сократил им время разработки. Но код HAL,- принадлежит Alt-у. По данному Вопросу, можно попробовать обратиться к директору компании. Но лучше сделать это через Сергея Серба. Он основной идеолог того HAL-а. Возможно он подскажет концепт.


  1. mbait
    00.00.0000 00:00

    Что такое "getAddress()" в данном коде?


  1. Denis42
    00.00.0000 00:00
    +3

    Как-то небезопасно. Банальные ioRegWrite() и ioRegRead() будут лучше в плане читаемости кода и его поддержки. В драйверах сложных устройств, куда более важна последовательность доступа к регистрам, правильное расположение барьеров и правильная работа с кэшем (в случае с DMA). Здесь запутанность кода с названиями вроде pllConfig.set(), содержащими неявную запись в биты HW регистров только вредит. А вот на уровне выше, да, здесь работа с классами/методами вроде pllConfig.setFrequencyMhz() будет куда удобнее.


    1. EvgenySbl Автор
      00.00.0000 00:00
      -2

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

      Для начала, задам себе вопрос. Когда может происходить скрытая запись? Когда это может навредить? Вероятно, в следующих случаях:

      1. Мы работаем с полем типа Write Only, и производим запись в регистр.

      2. Мы работаем с Reserved полем и производим запись в регистр.

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

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

      Какие еще варианты существуют? Предположим, регистр содержит поле типа WO. Не скрою, что с записью таких полей, существует проблема. Однако, программируя на регистрах, Вы никак ее не обойдете. Хочется сказать "обычно", или "в 90% случаев", но это анулирует навыки программиста как программиста, поскольку требуется учитывать все возможные варианты. Но в большинстве случаев, поле WO, это то, что вызывает начало каких-либо транзакций. Например старта DMA. Хотя это может быть поле из разряда установки или сброса битов в порту как у Atmel, в первых контроллерах на базе ARM, например. Вероятно, добавление значения по умолчанию, решит данный вопрос. Но в комментарии к статье, я пометил что код для работы с этими полями готов еще не весь. Вероятно, нужно подумать, значение по умолчанию, должно помочь и здесь. Однако, реализовать данный момент будет намного сложнее, т.к. нужно итерироваться по всем полям неинстанцированной структуры, унаследованной от определенного шаблонного класса.

      Возможно, Вы предложите свой вариант, когда это может вызвать проблему.

      Касательно барьеров. Они расположены правильно, с точки зрения операции пересылки данных между памятью и регистром. Однако, барьер может оказаться расположен перед операцией пересылки адреса MMIO регистра в регистр процессора. К чему это может привести? Максимум к прохождению значения регистра, через bypass в конвейере процессора, или задержки операции на несколько тактов, ввиду невозможности передачи через bypass. Касательно барьеров для WriteBuffer-а. Так добавьте их в функцию, они специально обозначены как preRead и postWrite.

      pllConfig.setFrequencyMhz( ...). Не отменяет работы с регистрами. Это более высокий уровень абстракции, требующий дополнительных вычислений значений.


      1. EvgenySbl Автор
        00.00.0000 00:00
        -1

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

        Вопрос к таким "персонажам". Вы можете прокомментировать, в чем конкретно, Вы не согласны с ответом. Или у Вас все по принципу "Кто не пляшет тот Москаль?". Я сейчас не в пользу Украины написал и не против нее. Я соблюдаю политическую нейтральность, как впрочем нейтрально отношусь к языку программирования на котором можно писать. Но, я сторонник раннего выявления ошибок и удобства, а в этом отношении лучше C++.

        Кстати, у меня предложение к администрации Хабра. Давайте добавим необходимость оставлять расширенный, подробный комментарий, скажем, если человек ставит минус более чем раз в несколько дней. И пусть его комментарий служит ответом на сообщение.

        Есть лозунг: критикушь,- предлагай. Что то я вижу минусы, а количество предложений стремится к нулю.


  1. Denis42
    00.00.0000 00:00

    Как-то небезопасно. Банальные ioRegWrite() и ioRegRead() будут лучше в плане читаемости кода и его поддержки. В драйверах сложных устройств, куда более важна последовательность доступа к регистрам, правильное расположение барьеров и правильная работа с кэшем (в случае с DMA). Здесь запутанность кода с названиями вроде pllConfig.set(), содержащими неявную запись в биты HW регистров только вредит. А вот на уровне выше, да, здесь работа с классами/методами вроде pllConfig.setFrequencyMhz() будет куда удобнее.


  1. kovserg
    00.00.0000 00:00
    +1

    Да уж. Самый яркий пример высокоуровневого апи для работы с портами можно посмотреть в windows. Они так улучшили работу с паралельным портом, что и перестали пользоваться.
    Что бы сделать то что на qbasic занимало 3 строчки:

    OUT 888, D%
    OUT 890, C%
    S% = INP(889)
    
    Теперь занимает тысячи строк с написание драйвера и другими квестами (например). А через штатное апи можно только передавать поток строго в нескольких режимах и то часть из них не реализована.

    Вообще все эти «получить доступ к нему, удобным способом» приводят к тому что помимо штатной документации, нужна еще одна которая описывает соответствие между тем и этим.
    Более того иногда что бы записать в регистр 0 надо записать туда 1 или предварительно еще ряд регистров поменять. И выстраивать симуляцию этого поведения на C++ особого смысла, кроме эстетического, нет. Достаточно что бы оно максимально соответствовало документации, остальное делается функциями, которые выполняют уже осмысленные действия и имеют внятные названия.


    1. EvgenySbl Автор
      00.00.0000 00:00

      С точки зрения прямого доступа к аппаратуре,- Вы безусловно правы. Работать с нестандартными устройствами напрямую, стало сложнее. Однако, если Вы можете подогнать аппартуру под определенный класс устройств, в ряде случаев даже драйвер писать не приходится, или достаточно написать фильтрующий.

      Вероятно, просто, в целях обеспечения безопасности, в целях стандартизации, переносимости, а так же возможности виртуализации, возник такой стек драйверов. Гипотетически ни что не мешает разработать драйвер, простого порта. Я не знаю что произошло с WDM моделью в Windows 10 или 11, однако в более ранних, Вы могли просто разработать драйвер ядра, который будет писать число в порт или читать оттуда, используя IRP_MJ_IO_CONTROL. При этом, нужна только реализация по умолчанию остальных PNP-запросов. В этом случае Вы смогли бы использовать на уровне ОС вызов ioctl(....), который будет эквивалентен OUT или IN. Но это автоматически обесценит всю систему безопасности операционной системы, т.к. любое приложение, открывшее ваше устройство сможет делать практически что угодно. Еще один минус, это обращение будет проходить через изменение режимов работы процессора. И еще один минус,- ваш код, вероятно не запуститься в гипервизоре.

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

      P.S.: Во времена DOS, еще до того как стал работать с МК, баловался с LPT портом, цепляя к нему светодиодную матрицу. Тогда, это было просто. А из языков программирования знал только Assember x86, Assembler Z80, Basic и Pascal. :-). Мне было 16 лет, а в настоящий момент 39.

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


      1. kovserg
        00.00.0000 00:00

        Или не увидим. В винде много чего есть learn.microsoft.com/en-us/windows-hardware/drivers/ddi/gpio что не работает.


  1. cdriper
    00.00.0000 00:00
    -2

    смотрю на реакции -- расшевелили вы болото консервативных твердолобых сишников которые "будем писать так, как писали деды наши и прадеды"


    1. lorc
      00.00.0000 00:00

      Скажите, вы когда-нибудь изучали memory model C++? Там в принципе нельзя сделать переносимый код для работы с memory mapped регистрами. Гарантии по memory ordering дают только атомики. А атомики для работы с регистрами не приспособишь.

      (великая тайна программистов C состоит в том, что на C тоже нельзя написать переносимый код для работы с регистрами. Точно по той же причине. Поэтому например в Linux есть "функции" типа readl/writel которые в конце концов разматываются в непереносимую ассемблерную вставку под каждую архитектуру отдельно).


      1. cdriper
        00.00.0000 00:00
        -1

        • большинство критики идет совсем по другому поводу

        • никто не мешает проверить переносимость под конкретный компилятор

        • никто не мешает реализовать нижний уровень на основе функций типа readl/writel


        1. lorc
          00.00.0000 00:00

          большинство критики идет совсем по другому поводу

          Я бы не сказал что прямо "большинство".

          никто не мешает проверить переносимость под конкретный компилятор

          Если мы нам надо проверять код под конкретный компилятор - то это уже не переносимый код.

          никто не мешает реализовать нижний уровень на основе функций типа readl/writel

          Это да. Заодно можно будет избавиться от reinterpret_cast. Что тру C++ программисты думают о reinterpret_cast, кстати?


          1. cdriper
            00.00.0000 00:00
            -1

            то это уже не переносимый код

            непереносимая часть прячется за абстракцию в которой уже реализуется код под ту или иную архитектуру/компилятор

            Что тру C++ программисты думают о reinterpret_cast, кстати?

            то, что есть вещи, которые без него реализовать невозможно


          1. mctMaks
            00.00.0000 00:00

            Что тру C++ программисты думают о reinterpret_cast, кстати?

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


  1. OverThink
    00.00.0000 00:00

    Промотрел код на гитхабе. Зачем указывать constexpr const, если constexpr переменные уже явно как const? Также constexpr функции уже явно inline.

    	struct RW {
    		static constexpr const AccessMode Policy = AccessMode::ReadWrite;
    		typedef Descr Description;
    		typedef typename Descr::FieldValueType Type;
    
    		static inline constexpr const AddressType getAddress() {
    			return address;
    		}
    
    		static inline const void set(const typename Descr::FieldValueType value) 

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

    static constexpr const uint32_t GPIOA_BASE = 0x40010800;
    static constexpr const uint32_t GPIOB_BASE = 0x40010C00;
    static constexpr const uint32_t GPIOC_BASE = 0x40011000;


    1. EvgenySbl Автор
      00.00.0000 00:00
      -1

      Спасибо за комментарий, мне нравится, когда кто-то дает вразумительные комментарии, особенно, если они указывают на потенциальные вопросы, пусть и не вызывющие никаких проблем в процессе работы.

      Зачем здесь используется ключевое слово static, если константные глобальные переменные имеют внутреннею связь по умолчанию?

      У меня встречный вопрос. Каким образом данный метод свзяан с глобальными переменными, или методами класса?

      В данном случае, он связан только с шаблонным параметром класса, который, в свою очередь, оказывается constexpr выражением, производным от парметра шаблона. Вероятно, лишний, тут inline. Если говрить о inline, то эта директива, в моем случае, обычно, используется в ряде методов, чтобы они не инстанцировались. Если бы вдруг address, оказался константной внутри класса. Обычно, такие константы в моем коде, начинаются с подчеркивания. В случае константы, это могло бы привести к его инстнцированию в некоторых случаях. Директива inline, помогает избежать этой процедуры, а сам метод, превращается в этом случае в команду чтения константы по адресу.

      static constexpr const AddressTpye getAddress()

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

      static constexpr const uint32_t GPIOA_BASE = 0x40010800;
      static constexpr const uint32_t GPIOB_BASE = 0x40010C00;
      static constexpr const uint32_t GPIOC_BASE = 0x40011000;

      Эта статья, и код из нее являлись экспериментом.

      Постараюсь вспомнить что было больше чем год назад. Вероятно, это рудимент. Данные константы, возможно, изначально были объявлены не внутри namespace-а, а внутри стурктры. Внутри структуры constexpr выражение, должно быть статическим. Другой вариант,- адреса были прописаны где то в header-е или C-файле, и изначально не являлись constexpr-выражениями. А static был нужен, чтобы к константам не было глобального доступа из других файлов, в которые не включается заголовок.

      Вероятно, Вас напрягаяет обилие const в коде. Просто мне нравится помечать константные выражения константами. Это повышает удобочитаемость. Кроме того, это всегда подсказка компилятору на этапе компиляции.

      Вопрос: Что плохого может быть в выражении constexpr const Type ?


      1. kovserg
        00.00.0000 00:00
        +2

        А что полохого в выражении:

        enum {
          GPIOA_BASE = 0x40010800,
          GPIOB_BASE = 0x40010C00,
          GPIOC_BASE = 0x40011000
        };
        


        1. EvgenySbl Автор
          00.00.0000 00:00

          Интересный вариант. У меня, ход мысли так не сработал. Обычно enum ассоциируется с перечислением однотипных вещей. И зачастую с последовательным без разрывов. Впрочем, GPIO порты тоже, как правило однотипные.

          На самом деле, насколько я понимаю, для компилятора нет большой разницы между #define GPIOA_BASE 0x40010800, и enum-ом с приваиванием. Хотя, Вы можете меня поправить.

          И снова,мне нравится строгая типизация, и в таком случае. Я обычно предпочитаю enum class. Его можно проверить. В вместо члена enum-а в ряде случаев можно будет подсунуть константу или define, и компилятор даже не ругнется.

          Да и элемент enum class автоматически разделен логически и синтаксически. Например BaseAddr::GpioA.


          1. EvgenySbl Автор
            00.00.0000 00:00

            Не получилось отредактировать сообщение. Хотел дописать что для Ареса, наверное лучше namespace. BaseAddr. Будет тот же BaseAddr::GpioA


            1. kovserg
              00.00.0000 00:00

              Разница между #define и enum огромна.

              #define A 7
              
              enum { B=1 };
              struct S { enum { B=2 }; };
              namespace N { enum { B=3 }; }
              void f() { enum { B=4 }; }
              

              И еще enum он constexpr. Огрести только с шаблонами и то только если постараться.


              1. EvgenySbl Автор
                00.00.0000 00:00

                Согласен, с оговоркой. Я имел ввиду одноранговые выражения, и специфику использования, при описании полей регистров, в типовых ситуациях. Т.е. как обычно в коде различных библиотек, использующих enum вместо define. Мы же в этом контексте общаемся? Вопрос заданный вне контекста и в контесксте, может иметь противоположные ответы.

                И еще enum он constexpr. 

                Что вы имеете ввиду? enum тянется и C, когда о constexpr выражениях еще небыло речи, т.к. в структуре языка еще небыло ключевого слова constexpr. Или я не прав?

                Огрести только с шаблонами и то только если постараться.

                Не совсем понял фразу. Можете написать, что имелось ввиду? Если можно,- в личку. Не хотелось бы вести баталии, которые уходят в сторону от темы здесь.


  1. OverThink
    00.00.0000 00:00

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