Pull to refresh

Comments 47

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

  • Я вижу что генерируете барьеры для процессора, но почему-то забыли про барьер для компилятора. Он же может и переставить инструкции записи/чтения местами. Обычно ставят что-то типа 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 можно и не парится.

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

Однако, еще ни разу не встречал чтобы компилятор переставил последовательность инструкций, если переменная явно объявлена как 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.

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

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

Да никто и не говорит что 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();
			}
		}

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

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

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

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

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

Однако, еще ни разу не встречал чтобы компилятор переставил последовательность инструкций, если переменная явно объявлена как 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 и все.

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

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

Ну я же просто так написал про все это, а исходя из собственного опыта. Я вообще занимаюсь активной разработкой и поддержкой 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 - это далеко не "единичные" инструкции.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Как посмотреть. Старый добрый 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".

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вероятно, просто, в целях обеспечения безопасности, в целях стандартизации, переносимости, а так же возможности виртуализации, возник такой стек драйверов. Гипотетически ни что не мешает разработать драйвер, простого порта. Я не знаю что произошло с 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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Интересный ответ. Да, конечно нельзя не считаться со стандартом.

Но посмотрите на библиотеки от Atmel, от STM, от NXP, да и многих других вендоров. Практически никто из них не использует в коде ассемблерные вставки для обращения к MMIO. Вообще говоря в некоторых архитектурах, насколько я помню, кроме Memory Mapped регистров, существуют регистры в IO пространстве, и в этом случае уже должны быть использованы другие команды. Возможно использование ассемблерных вставок имеет свои корни из за этой особенности?

Практически никто из них не использует в коде ассемблерные вставки для обращения к MMIO.

Ну тут есть два нюанса. Во-первых, эти библиотеки для ядер типа Cortex M0-М3 где нет MMU и нет спекулятивного чтения/записи памяти, соответственно, надо бороться только з оптимизациями компилятора, что куда проще. Во-вторых - эти библиотеки непереносимые. Ни между архитектурами (что очевидно), ни между компиляторами.

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

Ну с IO пространством - да, там без ассемблера не обойтись. Но ассемблерные вставки для memory mapped регистров все равно нужны в некоторых кейсах.

Например, рассмотрим архитектуру ARMv8-A, там есть MMU, память через MMU может быть замаплена в нескольких режимах. Для нас интересны два - это Normal Memory и Device Memory. Normal Memory - это обычная память, с ней можно работать как угодно, но она кешируемая и позволяет процессору делать спекулятивные обращения, что совершенно не подходит для memory mapped регистров, где даже чтение может иметь побочные эффекты. Поэтому есть режим Device Memory, где все наоборот - никаких кешей, никакого спекулятивного доступа. Но! Это не позволяет использовать, например, векторные инструкции для обращения к этой памяти. Банальная memcpy в линуксе роняет ядро, если пытаться копировать в/из device memory, потому что эта функция старается использовать самые быстрые инструкции для перемещения больших кусков памяти. Но эти инструкции нельзя применять для device memory.

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

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

Промотрел код на гитхабе. Зачем указывать 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;

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

Зачем здесь используется ключевое слово 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 ?

А что полохого в выражении:
enum {
  GPIOA_BASE = 0x40010800,
  GPIOB_BASE = 0x40010C00,
  GPIOC_BASE = 0x40011000
};

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

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

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

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

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

Разница между #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. Огрести только с шаблонами и то только если постараться.

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

И еще enum он constexpr. 

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

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

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

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

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

Sign up to leave a comment.

Articles