Как мне создать «разделитель» в структуре памяти класса C ++?

94

Проблема

В низкоуровневом встроенном контексте « голого металла» я хотел бы создать пустое пространство в памяти внутри структуры C ++ без какого-либо имени, чтобы запретить пользователю доступ к такой области памяти.

Прямо сейчас я добился этого, поместив уродливое uint32_t :96;битовое поле, которое удобно заменит три слова, но вызовет предупреждение от GCC (битовое поле слишком велико, чтобы поместиться в uint32_t), что вполне законно.

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

Как мне это сделать правильно?

Почему вообще возникает проблема?

Проект, над которым я работаю, состоит из определения структуры памяти различных периферийных устройств всей линейки микроконтроллеров (STMicroelectronics STM32). Для этого результатом является класс, который содержит объединение нескольких структур, которые определяют все регистры, в зависимости от целевого микроконтроллера.

Вот один простой пример довольно простого периферийного устройства: универсальный ввод / вывод (GPIO)

union
{

    struct
    {
        GPIO_MAP0_MODER;
        GPIO_MAP0_OTYPER;
        GPIO_MAP0_OSPEEDR;
        GPIO_MAP0_PUPDR;
        GPIO_MAP0_IDR;
        GPIO_MAP0_ODR;
        GPIO_MAP0_BSRR;
        GPIO_MAP0_LCKR;
        GPIO_MAP0_AFR;
        GPIO_MAP0_BRR;
        GPIO_MAP0_ASCR;
    };
    struct
    {
        GPIO_MAP1_CRL;
        GPIO_MAP1_CRH;
        GPIO_MAP1_IDR;
        GPIO_MAP1_ODR;
        GPIO_MAP1_BSRR;
        GPIO_MAP1_BRR;
        GPIO_MAP1_LCKR;
        uint32_t :32;
        GPIO_MAP1_AFRL;
        GPIO_MAP1_AFRH;
        uint32_t :64;
    };
    struct
    {
        uint32_t :192;
        GPIO_MAP2_BSRRL;
        GPIO_MAP2_BSRRH;
        uint32_t :160;
    };
};

Где all GPIO_MAPx_YYY- макрос, определяемый либо как, либо как uint32_t :32тип регистра (выделенная структура).

Здесь вы видите, uint32_t :192;что работает хорошо, но вызывает предупреждение.

Что я рассмотрел до сих пор:

Я мог бы заменить его несколькими uint32_t :32;(здесь 6), но у меня есть несколько крайних случаев, когда у меня есть uint32_t :1344;(42) (среди других). Так что я бы не стал добавлять около сотни строк поверх 8k других, даже если генерация структуры написана по сценарию.

Точное предупреждающее сообщение выглядит примерно так: width of 'sool::ll::GPIO::<anonymous union>::<anonymous struct>::<anonymous>' exceeds its type(Мне просто нравится, насколько это сомнительно).

Я бы предпочел не решать эту проблему, просто удаляя предупреждение, но используя

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-WTheRightFlag"
/* My code */
#pragma GCC diagnostic pop

может быть решение ... если найду TheRightFlag. Однако, как указано в этой теме , gcc/cp/class.cс этой печальной частью кода:

warning_at (DECL_SOURCE_LOCATION (field), 0,
        "width of %qD exceeds its type", field);

Это говорит нам, что нет -Wxxxфлага для удаления этого предупреждения ...

J Faucher
источник
26
ты считал char unused[12];и так далее?
MM
3
Я бы просто подавил предупреждение. [class.bit] / 1 гарантирует поведение uint32_t :192;.
NathanOliver 01
3
@NathanOliver Я бы тоже с радостью, но кажется, что это предупреждение нельзя подавить (с использованием GCC), или я не нашел, как это сделать. Более того, это все еще не чистый способ (но это было бы неплохо). Мне удалось найти правильный флаг «-W», но не удалось применить его только к моим собственным файлам (я не хочу, чтобы пользователь удалял подобные предупреждения для своей работы)
J Faucher,
3
Кстати, вы можете написать :42*32вместо:1344
MM
1
Пытаться подавить предупреждения? gcc.gnu.org/onlinedocs/gcc/…
Hitobat 01

Ответы:

36

Используйте несколько смежных анонимных битовых полей. Так что вместо:

    uint32_t :160;

например, у вас будет:

    uint32_t :32;
    uint32_t :32;
    uint32_t :32;
    uint32_t :32;
    uint32_t :32;

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

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

#define REPEAT_2(a) a a
#define REPEAT_4(a) REPEAT_2(a) REPEAT_2(a)
#define REPEAT_8(a) REPEAT_4(a) REPEAT_4(a)
#define REPEAT_16(a) REPEAT_8(a) REPEAT_8(a)
#define REPEAT_32(a) REPEAT_16(a) REPEAT_16(a)

Затем можно добавить пространство 1344 (42 * 32 бита) следующим образом:

struct
{
    ...
    REPEAT_32(uint32_t :32;) 
    REPEAT_8(uint32_t :32;) 
    REPEAT_2(uint32_t :32;)
    ...
};
Клиффорд
источник
Спасибо за ответ. Я уже думал об этом, однако это добавит более 200 строк в некоторые из моих файлов ( uint32_t :1344;есть на месте), поэтому я бы предпочел не идти этим путем ...
Дж. Фаучер
1
@JFaucher Добавил возможное решение для вашего требования к количеству строк. Если у вас есть такие требования, вы можете упомянуть их в вопросе, чтобы не получить ответы, которые им не соответствуют.
Клиффорд
Спасибо за редактирование и извините за то, что не указали счетчик строк. Я хочу сказать, что в мой код уже больно нырять, потому что там много строк, и я бы предпочел не добавлять слишком много. Поэтому я спрашивал, знает ли кто-нибудь «чистый» или «официальный» способ избежать использования соседнего анонимного битового поля (даже если это работает нормально). Однако мне кажется, что макро-подход подходит. Кстати, в вашем примере у вас нет места размером 36 * 32 бита?
J Faucher
@JFaucher - исправлено. Файлы сопоставления регистров ввода-вывода обязательно имеют большой размер из-за большого количества регистров - обычно вы пишете один раз, и обслуживание не является проблемой, потому что оборудование является постоянным. За исключением того, что вы «скрываете» регистры, вы сами выполняете работы по обслуживанию, если вам впоследствии понадобится к ним доступ. Вы, конечно, знаете, что все устройства STM32 уже имеют заголовок карты регистров, предоставленный поставщиком? Это было бы гораздо менее подвержено ошибкам.
Клиффорд
2
Я согласен с вами и, честно говоря, думаю, что я воспользуюсь одним из тех двух методов, которые указаны в вашем ответе. Я просто хотел убедиться, что C ++ не предлагает лучшего решения, прежде чем сделать это. Мне хорошо известно, что ST предоставляет эти заголовки, однако они построены на массовом использовании макросов и побитовых операций. Мой проект заключается в создании эквивалента C ++ для тех заголовков, которые будут менее подвержены ошибкам (с использованием классов перечисления, битовых полей и т. Д.). Вот почему мы используем сценарий для «перевода» заголовков CMSIS в наши структуры C ++ (и, кстати, обнаружили некоторые ошибки в файлах ST)
Дж. Фаучер,
45

Как насчет C ++ - ишского способа?

namespace GPIO {

static volatile uint32_t &MAP0_MODER = *reinterpret_cast<uint32_t*>(0x4000);
static volatile uint32_t &MAP0_OTYPER = *reinterpret_cast<uint32_t*>(0x4004);

}

int main() {
    GPIO::MAP0_MODER = 42;
}

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

геза
источник
1
Это могло бы быть менее оптимизировано, чем структура для доступа к нескольким регистрам MMIO из одной и той же функции. Имея указатель на базовый адрес в регистре, компилятор может использовать инструкции загрузки / сохранения с немедленным смещением, например ldr r0, [r4, #16], в то время как компиляторы с большей вероятностью пропустят эту оптимизацию с каждым адресом, объявленным отдельно. GCC, вероятно, загрузит каждый адрес GPIO в отдельный регистр. (Из буквального набора, хотя некоторые из них могут быть представлены как повернутые непосредственно в кодировке Thumb.)
Питер Кордес
4
Оказывается, мои опасения были напрасными; ARM GCC тоже оптимизируется таким образом. godbolt.org/z/ztB7hi . Но учтите, что хотите static volatile uint32_t &MAP0_MODER, а не inline. inlineПеременная не компилируется. ( staticпозволяет избежать статического хранилища для указателя, и volatileэто именно то, что вам нужно для MMIO, чтобы избежать устранения мертвого хранилища или оптимизации записи / чтения.)
Питер Кордес
1
@PeterCordes: встроенные переменные - это новая функция C ++ 17. Но вы правы, staticподходит и для этого случая. Спасибо за упоминание volatile, я добавлю его к своему ответу (и изменю встроенный на статический, чтобы он работал до C ++ 17).
geza 02
2
Это не строго определенное поведение, см. Эту
ветку в
1
@JFaucher: создайте столько пространств имен, сколько у вас есть структур, и используйте автономные функции в этом пространстве имен. Итак, у вас будет GPIOA::togglePin().
geza 02
20

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

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

Пример:

uint16_t * const UART1 = (uint16_t *)(0x40000);
const unsigned int UART_STATUS_OFFSET = 1U;
const unsigned int UART_TRANSMIT_REGISTER = 2U;
uint16_t * const UART1_STATUS_REGISTER = (UART1 + UART_STATUS_OFFSET);
uint16_t * const UART1_TRANSMIT_REGISTER = (UART1 + UART_TRANSMIT_REGISTER);

Вы также можете использовать обозначение массива:

uint16_t status = UART1[UART_STATUS_OFFSET];  

Если вы должны использовать структуру, IMHO, лучший способ пропустить адреса - это определить член и не обращаться к нему:

struct UART1
{
  uint16_t status;
  uint16_t reserved1; // Transmit register
  uint16_t receive_register;
};

В одном из наших проектов у нас есть как константы, так и структуры от разных поставщиков (поставщик 1 использует константы, а поставщик 2 использует структуры).

Томас Мэтьюз
источник
Спасибо за Ваш ответ. Однако я предпочитаю использовать структурный подход, чтобы облегчить работу пользователя, когда он получил функцию автозаполнения (у вас будут отображаться только правильные атрибуты), и я не хочу «показывать» пользователю зарезервированные слоты как указал в комментарии к моему первому посту.
J Faucher
Вы все еще можете иметь это, сделав указанные выше адресные staticчлены структуры, предполагая, что автозаполнение может отображать статические члены. В противном случае это также могут быть встроенные функции-члены.
Phil1970 02
@JFaucher Я не специалист по встроенным системам и не тестировал это, но разве проблема с автозаполнением не будет решена путем объявления зарезервированного участника закрытым? (Вы можете объявить частные члены в структуре, и вы можете использовать public:и private:столько раз, сколько захотите, чтобы получить правильный порядок полей.)
Натаниэль
1
@ Натаниэль: Не так; если в классе есть как статические, так publicи privateнестатические члены данных, то это не стандартный тип макета , поэтому он не обеспечивает гарантий упорядочения, о которых вы думаете. (И я почти уверен, что для варианта использования OP действительно требуется стандартный тип макета.)
ruakh
1
Не забудьте volatileоб этих объявлениях, BTW, для регистров ввода-вывода с отображением в память.
Питер Кордес
13

geza прав, что вы действительно не хотите использовать для этого классы.

Но, если вы настаиваете, лучший способ добавить неиспользуемый член шириной n байтов - это просто сделать это:

char unused[n];

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


Для GNU C / C ++ (gcc, clang и другие, поддерживающие те же расширения) одно из допустимых мест для размещения атрибута:

#include <stddef.h>
#include <stdint.h>
#include <assert.h>  // for C11 static_assert, so this is valid C as well as C++

struct __attribute__((packed)) GPIO {
    volatile uint32_t a;
    char unused[3];
    volatile uint32_t b;
};

static_assert(offsetof(struct GPIO, b) == 7, "wrong GPIO struct layout");

(пример в обозревателе компилятора Godbolt показывает offsetof(GPIO, b)= 7 байт.)

Гонки легкости на орбите
источник
9

Чтобы расширить ответы @ Clifford и @Adam Kotwasinski:

#define REP10(a)        a a a a a a a a a a
#define REP1034(a)      REP10(REP10(REP10(a))) REP10(a a a) a a a a

struct foo {
        int before;
        REP1034(unsigned int :32;)
        int after;
};
int main(void){
        struct foo bar;
        return 0;
}
Mosvy
источник
Я включил вариант вашего предложения в свой ответ, следуя дальнейшим требованиям в комментарии. Кредит, когда кредит подлежит оплате.
Клиффорд
7

Чтобы расширить ответ Клиффорда, вы всегда можете макрос анонимных битовых полей.

Так что вместо

uint32_t :160;

использовать

#define EMPTY_32_1 \
 uint32_t :32
#define EMPTY_32_2 \
 uint32_t :32;     \ // I guess this also can be replaced with uint64_t :64
 uint32_t :32
#define EMPTY_32_3 \
 uint32_t :32;     \
 uint32_t :32;     \
 uint32_t :32
#define EMPTY_UINT32(N) EMPTY_32_ ## N

А затем используйте это как

struct A {
  EMPTY_UINT32(3);
  /* which resolves to EMPTY_32_3, which then resolves to real declarations */
}

К сожалению, вам понадобится столько EMPTY_32_Xвариантов, сколько у вас есть байтов :( Тем не менее, это позволяет вам иметь отдельные объявления в вашей структуре.

Адам Котвасински
источник
5
Я думаю, что, используя макросы Boost CPP, вы можете использовать рекурсию, чтобы не создавать вручную все необходимые макросы.
Питер Кордес
3
Вы можете каскадировать их (до предела рекурсии препроцессора, но обычно этого достаточно). So #define EMPTY_32_2 EMPTY_32_1; EMPTY_32_1and #define EMPTY_32_3 EMPTY_32_2; EMPTY_32_1etc.
Miral
Возможно, @PeterCordes, но теги указывают, что, возможно, требуется совместимость с C и C ++.
Клиффорд
2
C и C ++ используют один и тот же препроцессор C; Я не вижу другой проблемы, кроме как сделать необходимый заголовок boost доступным для C. Они действительно помещают материал CPP-макроса в отдельный заголовок.
Питер Кордес
1

Чтобы определить большой разделитель как группы по 32 бита.

#define M_32(x)   M_2(M_16(x))
#define M_16(x)   M_2(M_8(x))
#define M_8(x)    M_2(M_4(x))
#define M_4(x)    M_2(M_2(x))
#define M_2(x)    x x

#define SPACER int : 32;

struct {
    M_32(SPACER) M_8(SPACER) M_4(SPACER)
};
jxh
источник
1

Думаю, было бы полезно ввести еще какую-то структуру; что, в свою очередь, может решить проблему проставок.

Назовите варианты

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

Поэтому в качестве первого шага я бы подумал о том, чтобы выделитьstruct :

// GpioMap0.h
#pragma once

// #includes

namespace Gpio {
struct Map0 {
    GPIO_MAP0_MODER;
    GPIO_MAP0_OTYPER;
    GPIO_MAP0_OSPEEDR;
    GPIO_MAP0_PUPDR;
    GPIO_MAP0_IDR;
    GPIO_MAP0_ODR;
    GPIO_MAP0_BSRR;
    GPIO_MAP0_LCKR;
    GPIO_MAP0_AFR;
    GPIO_MAP0_BRR;
    GPIO_MAP0_ASCR;
};
} // namespace Gpio

// GpioMap1.h
#pragma once

// #includes

namespace Gpio {
struct Map1 {
    // fields
};
} // namespace Gpio

// ... others headers ...

И наконец, глобальный заголовок:

// Gpio.h
#pragma once

#include "GpioMap0.h"
#include "GpioMap1.h"
// ... other headers ...

namespace Gpio {
union Gpio {
    Map0 map0;
    Map1 map1;
    // ... others ...
};
} // namespace Gpio

Теперь я могу написать void special_map0(Gpio:: Map0 volatile& map); , а также получить краткий обзор всех доступных архитектур с первого взгляда.

Простые распорки

Благодаря разделению определения на несколько заголовков, заголовки становятся более управляемыми по отдельности.

Поэтому мой первоначальный подход к точному соответствию вашим требованиям заключался бы в повторении std::uint32_t:32;. Да, он добавляет несколько строк по 100 к существующим строкам 8k, но поскольку каждый заголовок индивидуально меньше, это может быть не так плохо.

Но если вы готовы рассмотреть более экзотические решения ...

Представляем $.

Это малоизвестный факт, который $является жизнеспособным символом для идентификаторов C ++; это даже жизнеспособный начальный символ (в отличие от цифр).

$Появляются в исходном коде, вероятно , поднять брови, и $$$$, безусловно , будет привлекать к себе внимание во время обзора кода. Это то, чем вы легко можете воспользоваться:

#define GPIO_RESERVED(Index_, N_) std::uint32_t $$$$##Index_[N_];

struct Map3 {
    GPIO_RESERVED(0, 6);
    GPIO_MAP2_BSRRL;
    GPIO_MAP2_BSRRH;
    GPIO_RESERVED(1, 5);
};

Вы даже можете собрать простой «линт» в качестве ловушки перед фиксацией или в вашем CI, который ищет $$$$в зафиксированном коде C ++ и отклоняет такие фиксации.

Матье М.
источник
1
Помните, что конкретный вариант использования OP предназначен для описания регистров ввода-вывода с отображением памяти для компилятора. Он никогда не имеет смысла скопировать всю - структуру по значению. (И каждый член вроде бы, GPIO_MAP0_MODERпо-видимому, есть volatile.) Возможно, использование ссылки или параметра шаблона ранее анонимных членов могло бы быть полезным. И для общего случая структур заполнения, конечно. Но вариант использования объясняет, почему OP оставил их анонимными.
Питер Кордес
Вы можете использовать, $$$padding##Index_[N_];чтобы сделать имя поля более понятным, если оно когда-либо появлялось при автозаполнении или при отладке. (Или zz$$$paddingдля сортировки по GPIO...именам, потому что весь смысл этого упражнения в соответствии с OP - более приятное автозаполнение для отображенных в памяти имен местоположений ввода-вывода.)
Питер Кордес
@PeterCordes: Я снова просмотрел ответ, чтобы проверить, и никогда не видел упоминания о копировании. Однако я забыл volatileквалификатор в ссылке, который был исправлен. Что касается наименования; Я доведу его до ОП. Есть много вариантов (заполнение, зарезервировано, ...), и даже «лучший» префикс для автозаполнения может зависеть от имеющейся IDE, хотя я ценю идею настройки сортировки.
Matthieu M.
Я имел в виду « и нет простого способа передать все связанные поля вместе », что звучит как назначение структуры, и остальную часть предложения об именовании элементов структуры объединения.
Питер Кордес
1
@PeterCordes: Я думал о переходе по ссылке, как показано ниже. Мне неловко, что структура OP не позволяет им создавать «модули», которые могут быть статически доказаны для доступа только к определенной архитектуре (путем ссылки на конкретную struct), и что в unionконечном итоге они распространяются повсюду даже в специфичных для архитектуры битах, которые может меньше заботиться о других.
Matthieu M.
0

Хотя я согласен, что структуры не должны использоваться для доступа к портам ввода-вывода MCU, исходный вопрос можно ответить следующим образом:

struct __attribute__((packed)) test {
       char member1;
       char member2;
       volatile struct __attribute__((packed))
       {
       private:
              volatile char spacer_bytes[7];
       }  spacer;
       char member3;
       char member4;
};

Вам может потребоваться заменить __attribute__((packed))на #pragma packили аналогичный в зависимости от синтаксиса вашего компилятора.

Смешивание частных и общедоступных членов в структуре обычно приводит к тому, что такой макет памяти больше не гарантируется стандартом C ++. Однако, если все нестатические члены структуры являются частными, она по-прежнему считается POD / стандартным макетом, как и структуры, которые их встраивают.

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

Обратите внимание, что spacerчлен сам по себе не является частным, поэтому доступ к данным все же можно получить следующим образом:

(char*)(void*)&testobj.spacer;

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

Джек Уайт
источник
1
Пользователи не могут объявлять идентификаторы в любом пространстве имен, содержащем двойные подчеркивания в любом месте имени (в C ++ или только в начале в C); это делает код некорректным. Эти имена зарезервированы для реализации и поэтому теоретически могут конфликтовать с вашими ужасно тонкими и причудливыми способами. В любом случае компилятор не обязан хранить ваш код, если он их содержит. Такие имена - не быстрый способ получить «внутренние» имена для собственного использования.
underscore_d
Спасибо, исправил.
Джек Уайт
-1

Анти-раствор.

НЕ ДЕЛАЙТЕ ЭТОГО: смешивайте частные и публичные поля.

Может быть, вам пригодится макрос со счетчиком для генерации уникальных имен переменных?

#define CONCAT_IMPL( x, y ) x##y
#define MACRO_CONCAT( x, y ) CONCAT_IMPL( x, y )
#define RESERVED MACRO_CONCAT(Reserved_var, __COUNTER__) 


struct {
    GPIO_MAP1_CRL;
    GPIO_MAP1_CRH;
    GPIO_MAP1_IDR;
    GPIO_MAP1_ODR;
    GPIO_MAP1_BSRR;
    GPIO_MAP1_BRR;
    GPIO_MAP1_LCKR;
private:
    char RESERVED[4];
public:
    GPIO_MAP1_AFRL;
    GPIO_MAP1_AFRH;
private:
    char RESERVED[8];
};
Роберт Анджейук
источник
3
Хорошо. Если никто не возражает, я оставлю ответ, чего не следует делать.
Роберт Анджейук
4
@NicHartley Учитывая количество ответов, мы близки к "исследовательскому" вопросу. В исследованиях знание тупиковых ситуаций по-прежнему остается знанием, оно помогает избежать того, чтобы другие пошли неверным путем. +1 за храбрость.
Oliv
1
@Oliv И я сделал -1, потому что OP чего-то требовал, этот ответ нарушил требование, и поэтому это плохой ответ. Я явно не высказывал никаких оценочных суждений, положительных или отрицательных, в отношении этого человека ни в одном из комментариев - только в отношении ответа. Я думаю, мы оба можем согласиться, что это плохо. То, что это говорит о человеке, не по теме этого сайта. (Хотя ИМО, любой, кто хочет потратить какое-то время, чтобы внести свой вклад в идею, делает что-то правильно, даже если идея не сработает)
Иск Фонд Моники
2
Да, это неправильный ответ. Но я боюсь, что некоторые люди могут прийти к такому же мнению. Из-за комментария и ссылки я только что кое-что узнал, для меня это не отрицательный момент.
Роберт Анджейук