Я пишу код для конкретного чипа stm32 с периферийными устройствами, отображаемыми в памяти. Я случайно знаю, что один из портов GPIO, GPIOA, контролируется несколькими 32-битными целыми числами, расположенными по адресу 0x40018000. Как я могу выразить эту информацию во время компиляции?
Моя первоначальная попытка выглядела так:
struct Gpio {
volatile uint32_t crl;
volatile uint32_t crh;
// ...
};
constinit Gpio& GPIOA = *reinterpret_cast<Gpio*>(0x40018000);
Кажется, это разумный способ сказать: «Ссылка GPIOA относится к объекту, расположенному по адресу памяти 0x40018000».
К сожалению, это не работает:
error: 'constinit' variable 'GPIOA' does not have a constant initializer
error: 'reinterpret_cast' from integer to pointer
Какие варианты мне доступны? Как я могу записать определенный числовой адрес памяти во время компиляции?
Предыдущие исследования:
@AviBerger BSP представляют собой библиотеки C и требуют слишком больших затрат во время выполнения. OP должен работать тоньше и быстрее, чем C.
Так должно быть constinit? Возможно, Gpio& GPIOA = *new(reinterpret_cast<Gpio*>(0x40018000)) Gpio; будет достаточно?
Индустрии встраиваемых систем необходимо рассмотреть возможность метапрограммирования C++ для оптимизации и безопасности библиотек. Лучшие решения – не самые простые. Я бы посчитал класс моногосударства лучшим дизайном, чем глобальную ссылку. Более того, нужно подумать об инициализации и о том, как избежать нежелательной многократной инициализации регистров управления. Решение предполагает более сложные конструкции. В противном случае будут ненужные затраты памяти.
@Red.Wave Вы правы, с моей стороны плохая терминология. Я думал о чем-то гораздо более ограниченном и базовом. По сути, это настройка для сопоставления структурных переменных с периферийными устройствами, отображаемыми в памяти, и не более того, как пытается сделать OP. Я пользовался такими вещами. Теперь, когда я думаю об этом, мне кажется, что у меня нет подходящего имени, которое можно было бы использовать для этого.
Поставщик предоставляет заголовок stm32xxxxx.h для конкретной детали, который определяет регистры MCU. Он даже определил символ GPIOA, поэтому, если вы без необходимости решите создать свой собственный, вам следует использовать пространство имен или, по крайней мере, уникальный префикс.
@Clifford Конечно, мой настоящий код находится в пространстве имен. Это не имело отношения к вопросу о том, как разместить переменные по конкретным адресам.
@Filipp, заголовок поставщика делает во многом то же, что вы сделали с приведением C и связью C для совместимости языков.





Я не знаю, как это сделать на C++, но знаю, как это сделать на C. Я думаю, будет что-то такое же.
Для С,
вы можете использовать ключевое слово атрибута компилятора, чтобы поместить переменную по указанному адресу. например:
int var __attribute__((section(".ARM.__at_0x20000000"))) = 0x1234;
но он передает сценарий ссылки и компилятор ARM.
для этого вы можете использовать скрипт компоновщика.
укажите имя раздела в коде.
int var __attribute__((section(".mysection"))) = 0x1234;
а затем в ваших скриптах ссылок разместить этот раздел по нужным вам адресам.
Оба не являются общими для C. В частности, это расширение GCC (и расширение Clang по совместимости).
В настоящее время язык не позволяет получить указатель/ссылку на такой объект как переменную во время компиляции. Единственный способ сформировать указатель — использовать reinterpret_cast, и это лишает инициализацию статуса постоянного выражения.
Однако не похоже, что вам действительно нужна инициализация во время компиляции. Если вас устраивает риск динамической инициализации, то constinit можно удалить, и компилятор больше не будет жаловаться. Это проблема только в том случае, если вы намерены использовать указатель/ссылку во время инициализации другой нелокальной переменной. В этом случае вы потенциально рискуете столкнуться с фиаско статического порядка инициализации.
Поведение указателей, преобразованных из целочисленных значений, полностью определяется реализацией (за исключением двустороннего приведения). Однако ничто в показанном вами коде на самом деле не создает объект по адресу, представленному указателем. Хотя поведение определяется реализацией, кажется разумным, что вам, вероятно, следует использовать Placement-new, чтобы правильно создать объект по указанному ранее адресу и получить указатель на этот объект.
Так, например:
Gpio& GPIOA = *std::construct_at(reinterpret_cast<Gpio*>(0x40018000));
Как отметил @TedLyngmo, использование std::construct_at приведет к инициализации объекта по значению, что, судя по тому, как вы определили класс, вероятно, приведет к тому, что все ваши члены будут инициализированы нулями. Если вы не хотите, чтобы этот доступ произошел, вам нужно использовать инициализацию по умолчанию, для чего вам нужно явное размещение-new:
Gpio& GPIOA = *::new(reinterpret_cast<void*>(0x40018000)) GPIOA;
Формально это не дает доступа к подобъектам-членам, но все равно приводит к тому, что они имеют неопределенное состояние. Поэтому бесполезно, если предполагается сохранение некоторого начального состояния этой памяти. В стандарте нет никакого понятия об этом. По стандарту в памяти всегда должен быть объект, который действительно содержит значение.
Возможно, стоит отметить, что при использовании construct_at вместо необработанного размещения new будет записываться 0 в volatileuint32_t в Gpio. Это может заставить оборудование что-то делать.... :-)
Действительно, эти регистры имеют четко определенное начальное состояние. Возможно, start_lifetime_as было бы более уместно.
@Filipp Да, это тоже сработает, но я еще не видел ни одной библиотеки, поддерживающей это, поэтому необработанное размещение new, вероятно, лучший вариант на сегодняшний день.
@TedLyngmo Да, я не думал об этом. Добавил примечание.
@Filipp Да, если вы используете новое размещение, то формально значение объекта будет неопределенным. Он не будет иметь никакого значения, полученного из битового состояния до нового размещения. Я не уверен, будет ли start_lifetime_at тогда гарантированно вести себя должным образом. Но в любом случае все это изначально определяется реализацией. На самом деле невозможно принять правильное решение без спецификации от поставщика компилятора. Но на практике этого не существует и никто не считает, что объекты нужно создавать правильно.
Оказывается, существует простой способ разместить глобальную переменную по определенному адресу.
Можно напрямую задать адрес символа с помощью скриптов компоновщика. В C++ просто объявите переменную следующим образом:
extern Gpio GPIO;
Не определяйте эту переменную ни в одном файле .cpp.
Затем в скрипте компоновщика укажите ему адрес:
GPIO = 0x40018000;
При связывании скажите компоновщику использовать ваш скрипт, используя опцию -T.
Теперь ваша программа должна компоноваться без каких-либо неопределенных ошибок ссылок. Символ будет относиться к указанной вами ячейке памяти. Единственное ограничение заключается в том, что для этого значения не будет никакой инициализации, но это желательно для регистра, отображаемого в памяти.
Я бы рассматривал это за пределами стандартного переносимого кода. Он по своей сути специфичен и непереносим. 1) Почему проблемой является время компоновки, а не время компиляции? Время соединения — это обычное время назначения адреса для встроенного кода на «голом железе». 2) Обычно я делаю это на C, а не на C++. Как правило, это были методы, специфичные для цепочки инструментов. Я видел, как это делалось с использованием: специфических атрибутов компилятора в исходном коде; приведения указателей в исходном коде; конфигурации компоновщика; Модули ассемблерного кода.