Como faço para criar um “espaçador” em uma estrutura de memória de classe C ++?


94

O problema

Em um contexto embarcado bare-metal de baixo nível , gostaria de criar um espaço em branco na memória, dentro de uma estrutura C ++ e sem nenhum nome, para proibir o usuário de acessar esse local de memória.

No momento, consegui colocar um campo de uint32_t :96;bits feio que ocupará convenientemente o lugar de três palavras, mas gerará um aviso do GCC (Bitfield muito grande para caber em uint32_t), o que é bastante legítimo.

Embora funcione bem, não é muito claro quando você deseja distribuir uma biblioteca com várias centenas desses avisos ...

Como faço isso corretamente?

Por que existe um problema em primeiro lugar?

O projeto no qual estou trabalhando consiste em definir a estrutura de memória de diferentes periféricos de uma linha inteira de microcontroladores (STMicroelectronics STM32). Para fazer isso, o resultado é uma classe que contém uma união de várias estruturas que definem todos os registros, dependendo do microcontrolador alvo.

Um exemplo simples para um periférico bastante simples é o seguinte: um General Purpose Input / Output (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;
    };
};

Onde tudo GPIO_MAPx_YYYé uma macro, definida como uint32_t :32ou o tipo de registro (uma estrutura dedicada).

Aqui você vê o uint32_t :192;que funciona bem, mas dispara um aviso.

O que eu considerei até agora:

Eu posso ter substituído por vários uint32_t :32;(6 aqui), mas tenho alguns casos extremos em que o fiz uint32_t :1344;(42) (entre outros). Portanto, prefiro não adicionar cerca de cem linhas em cima de outras 8k, embora a geração da estrutura seja feita por script.

A mensagem de aviso exata é algo como: width of 'sool::ll::GPIO::<anonymous union>::<anonymous struct>::<anonymous>' exceeds its type(Eu adoro o quão sombrio é).

Prefiro não resolver isso simplesmente removendo o aviso, mas usando

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

pode ser uma solução ... se eu encontrar TheRightFlag. No entanto, como apontado neste tópico , gcc/cp/class.ccom esta triste parte do código:

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

O que nos diz que não há -Wxxxsinalização para remover este aviso ...


26
você considerou char unused[12];e assim por diante?
MM

3
Eu simplesmente suprimiria o aviso. [class.bit] / 1 garante o comportamento de uint32_t :192;.
NathanOliver 01 de

3
@NathanOliver Eu adoraria também, mas parece que esse aviso não pode ser suprimido (usando o GCC) ou não descobri como fazer isso. Além disso, ainda não é uma maneira limpa de fazer (mas seria muito satisfatório). Consegui encontrar o sinalizador "-W" correto, mas não consegui aplicá-lo apenas em meus próprios arquivos (não quero que o usuário remova esse tipo de advertência para seu trabalho)
J Faucher

3
BTW, você pode escrever em :42*32vez de:1344
MM

1
Tente isso para suprimir avisos? gcc.gnu.org/onlinedocs/gcc/…
Hitobat 01 de

Respostas:


36

Use vários campos de bits anônimos adjacentes. Então, em vez de:

    uint32_t :160;

por exemplo, você teria:

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

Um para cada registro que você deseja que seja anônimo.

Se você tiver grandes espaços para preencher, pode ser mais claro e menos sujeito a erros usar macros para repetir o único espaço de 32 bits. Por exemplo, dado:

#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)

Em seguida, um espaço de 1344 (42 * 32 bits) pode ser adicionado assim:

struct
{
    ...
    REPEAT_32(uint32_t :32;) 
    REPEAT_8(uint32_t :32;) 
    REPEAT_2(uint32_t :32;)
    ...
};

Obrigado pela resposta. Já considerei isso, no entanto, acrescentaria mais de 200 linhas em alguns dos meus arquivos ( uint32_t :1344;está no local), então prefiro não ter que ir por aqui ...
J Faucher

1
@JFaucher Adicionou uma possível solução para seu requisito de contagem de linha. Se você tiver esses requisitos, poderá mencioná-los na pergunta para evitar obter respostas que não os atendam.
Clifford

Obrigado pela edição e desculpe por não declarar a coisa da contagem de linhas. Meu ponto é que meu código já é doloroso para mergulhar, pois há muitas linhas e prefiro evitar adicionar muito mais. Portanto, eu estava perguntando se alguém conhecia uma maneira "limpa" ou "oficial" de evitar o uso de um campo de bits anônimo adjacente (mesmo que funcione bem). A abordagem macro parece boa para mim. A propósito, no seu exemplo, você não tem um espaço de 36 * 32 bits?
J Faucher

@JFaucher - corrigido. Os arquivos de mapeamento de registro de E / S são necessariamente grandes devido ao grande número de registros - normalmente você escreve uma vez e a manutenção não é um problema porque o hardware é uma constante. Exceto por "ocultar" os registros, você está fazendo o trabalho de manutenção para si mesmo se precisar acessá-los posteriormente. Você está ciente, é claro, de que todos os dispositivos STM32 já possuem um cabeçalho de mapa de registro fornecido pelo fornecedor? Seria muito menos sujeito a erros usar isso.
Clifford

2
Concordo com você e, para ser justo, acho que irei por um dos dois métodos apresentados em sua resposta. Eu só queria ter certeza de que C ++ não oferece uma solução melhor antes de fazer isso. Estou bem ciente de que o ST fornece esses cabeçalhos, no entanto, eles são construídos sobre o uso massivo de macros e operações bit a bit. Meu projeto é construir um C ++ equivalente aos cabeçalhos que serão menos sujeitos a erros (usando classes enum, bitfields e assim por diante). É por isso que usamos um script para "traduzir" os cabeçalhos CMSIS em nossas estruturas C ++ (e encontramos alguns erros nos arquivos ST aliás)
J Faucher

45

Que tal uma maneira C ++ ish?

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;
}

Você obtém autocompletar por causa do GPIOnamespace e não há necessidade de preenchimento fictício. Mesmo, é mais claro o que está acontecendo, como você pode ver o endereço de cada registrador, você não precisa confiar no comportamento de preenchimento do compilador.


1
Isso pode otimizar menos bem do que uma estrutura para acesso a vários registros MMIO da mesma função. Com um ponteiro para o endereço base em um registrador, o compilador pode usar instruções load / store com deslocamentos imediatos, como ldr r0, [r4, #16], enquanto os compiladores são mais propensos a perder essa otimização com cada endereço declarado separadamente. O GCC provavelmente carregará cada endereço GPIO em um registro separado. (De um pool literal, embora alguns deles possam ser representados como imediatos girados na codificação Thumb.)
Peter Cordes

4
Acontece que minhas preocupações eram infundadas; O ARM GCC também otimiza dessa forma. godbolt.org/z/ztB7hi . Mas note que você quer static volatile uint32_t &MAP0_MODER, não inline. Uma inlinevariável não compila. ( staticevita ter qualquer armazenamento estático para o ponteiro, e volatileé exatamente o que você deseja para o MMIO para evitar a eliminação do armazenamento morto ou a otimização da gravação / releitura.)
Peter Cordes

1
@PeterCordes: variáveis ​​inline é um novo recurso do C ++ 17. Mas você está certo, staticfunciona igualmente bem neste caso. Obrigado por mencionar volatile, vou adicioná-lo à minha resposta (e mudar inline para estático, para que funcione para pré C ++ 17).
geza

2
Este não é um comportamento estritamente bem definido, consulte este tópico do Twitter e talvez este seja útil
Shafik Yaghmour

1
@JFaucher: crie tantos namespaces quantos structs você tiver e use funções autônomas nesse namespace. Então, você terá GPIOA::togglePin().
geza

20

Na arena de sistemas embarcados, você pode modelar hardware usando uma estrutura ou definindo ponteiros para os endereços de registro.

A modelagem por estrutura não é recomendada porque o compilador pode adicionar preenchimento entre os membros para fins de alinhamento (embora muitos compiladores para sistemas embarcados tenham um pragma para empacotar a estrutura).

Exemplo:

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);

Você também pode usar a notação de matriz:

uint16_t status = UART1[UART_STATUS_OFFSET];  

Se você deve usar a estrutura, IMHO, o melhor método para pular endereços seria definir um membro e não acessá-lo:

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

Em um de nossos projetos, temos constantes e estruturas de diferentes fornecedores (o fornecedor 1 usa constantes enquanto o fornecedor 2 usa estruturas).


Obrigado pela sua resposta. No entanto, optei por usar uma abordagem de estrutura para facilitar o trabalho do usuário quando ele obteve um recurso de autocompletar (você apenas terá os atributos corretos exibidos) e não quero "mostrar" ao usuário os slots reservados como apontou em um comentário do meu primeiro post.
J Faucher

Você ainda pode ter isso tornando os staticmembros de endereço acima de uma estrutura, assumindo que o preenchimento automático é capaz de mostrar membros estáticos. Caso contrário, também podem ser funções de membro embutidas.
Phil1970

@JFaucher Não sou uma pessoa de sistemas embarcados e não testei isso, mas o problema do preenchimento automático não seria resolvido declarando o membro reservado privado? (Você pode declarar membros privados em um struct, e você pode usar public:e private:como muitas vezes quiser, para obter a ordem correta dos campos.)
Nathaniel

1
@Nathaniel: Não; se uma classe tiver membros de dados não estáticos publice ambos private, não será um tipo de layout padrão , portanto, não fornece as garantias de ordenação nas quais você está pensando. (E tenho quase certeza de que o caso de uso do OP requer um tipo de layout padrão.)
ruakh

1
Não se esqueça volatiledessas declarações, BTW, para registradores de E / S mapeados em memória.
Peter Cordes

13

geza está certo que você realmente não quer usar classes para isso.

Mas, se você insistir, a melhor maneira de adicionar um membro não utilizado de largura de n bytes é simplesmente fazer isso:

char unused[n];

Se você adicionar um pragma específico da implementação para evitar a adição de preenchimento arbitrário aos membros da classe, isso pode funcionar.


Para GNU C / C ++ (gcc, clang e outros que suportam as mesmas extensões), um dos locais válidos para colocar o atributo é:

#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");

(exemplo no explorador do compilador Godbolt mostrando offsetof(GPIO, b)= 7 bytes).


9

Para expandir as respostas de @ Clifford e @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;
}

Eu incorporei uma variante de sua sugestão em minha resposta, seguindo outros requisitos em um comentário. Crédito onde o crédito é devido.
Clifford

7

Para expandir a resposta de Clifford, você sempre pode eliminar os campos de bits anônimos em macro.

Então, em vez de

uint32_t :160;

usar

#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

E então use como

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

Infelizmente, você precisará de tantas EMPTY_32_Xvariantes quantos bytes tiver :( Ainda assim, ele permite que você tenha declarações únicas em sua estrutura.


5
Usando macros Boost CPP, acho que você pode usar recursão para evitar ter que criar manualmente todas as macros necessárias.
Peter Cordes

3
Você pode colocá-los em cascata (até o limite de recursão do pré-processador, mas isso geralmente é amplo). Assim #define EMPTY_32_2 EMPTY_32_1; EMPTY_32_1e #define EMPTY_32_3 EMPTY_32_2; EMPTY_32_1etc.
Miral

@PeterCordes talvez, mas as tags indicam que talvez seja necessária compatibilidade com booth C e C ++.
Clifford

2
C e C ++ usam o mesmo pré-processador C; Não vejo outro problema além de disponibilizar o cabeçalho de reforço necessário para C. Eles colocam o material da macro CPP em um cabeçalho separado.
Peter Cordes

1

Para definir um grande espaçador como grupos de 32 bits.

#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)
};

1

Acho que seria benéfico introduzir mais estrutura; o que pode, por sua vez, resolver o problema dos espaçadores.

Nomeie as variantes

Embora namespaces simples sejam bons, o problema é que você acaba com uma coleção heterogênea de campos e nenhuma maneira simples de passar todos os campos relacionados juntos. Além disso, ao usar estruturas anônimas em uma união anônima, você não pode passar referências às próprias estruturas ou usá-las como parâmetros de modelo.

Como uma primeira etapa, eu consideraria, portanto, separar ostruct :

// 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 ...

E, finalmente, o cabeçalho global:

// Gpio.h
#pragma once

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

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

Agora, posso escrever um void special_map0(Gpio:: Map0 volatile& map); , bem como obter uma visão geral rápida de todas as arquiteturas disponíveis.

Espaçadores Simples

Com a definição dividida em vários cabeçalhos, os cabeçalhos são individualmente muito mais gerenciáveis.

Portanto, minha abordagem inicial para atender exatamente aos seus requisitos seria continuar repetindo std::uint32_t:32;. Sim, ele adiciona algumas linhas 100s às 8k linhas existentes, mas como cada cabeçalho é individualmente menor, pode não ser tão ruim.

Se você está disposto a considerar soluções mais exóticas, entretanto ...

Apresentando $.

É um fato pouco conhecido que $é um caractere viável para identificadores C ++; é até mesmo um personagem inicial viável (ao contrário dos dígitos).

Uma $aparição no código-fonte provavelmente levantaria sobrancelhas e $$$$definitivamente atrairá a atenção durante as revisões de código. Isso é algo que você pode facilmente aproveitar:

#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);
};

Você pode até mesmo montar um "lint" simples como um gancho de pré-confirmação ou em seu CI que procura $$$$no código C ++ confirmado e rejeita tais confirmações.


1
Lembre-se de que o caso de uso específico do OP é para descrever os registros de E / S mapeados na memória para o compilador. Ele não faz sentido para copiar toda a struct por valor. (E cada membro gosta GPIO_MAP0_MODERé presumivelmente volatile.) Possivelmente, referência ou uso de parâmetro de modelo de membros anteriormente anônimos pode ser útil. E para o caso geral de estruturas de enchimento, com certeza. Mas o caso de uso explica por que o OP os deixou anônimos.
Peter Cordes

Você pode usar $$$padding##Index_[N_];para tornar o nome do campo mais autoexplicativo caso ele tenha surgido no preenchimento automático ou durante a depuração. (Ou zz$$$paddingpara classificá-lo após GPIO...nomes, porque todo o objetivo deste exercício de acordo com o OP é o preenchimento automático mais agradável para nomes de localização de E / S mapeados em memória.)
Peter Cordes

@PeterCordes: Eu escanei a resposta novamente para verificar e nunca vi qualquer menção a cópia. Eu esqueci o volatilequalificador na referência, porém, que foi corrigido. Quanto à nomeação; Vou deixar isso para o OP. Existem muitas variações (preenchimento, reservado, ...) e até mesmo o "melhor" prefixo para preenchimento automático pode depender do IDE em mãos, embora eu aprecie a ideia de ajustar a classificação.
Matthieu M.

Eu estava me referindo a " e nenhuma maneira simples de passar todos os campos relacionados juntos ", que soa como atribuição de estrutura, e o resto da frase sobre como nomear os membros de estrutura do sindicato.
Peter Cordes

1
@PeterCordes: Estava pensando em passar por referência, conforme ilustrado mais adiante. Acho estranho que a estrutura do OP os impeça de criar "módulos" que podem ser estaticamente comprovados para acessar apenas uma arquitetura específica (tomando uma referência ao específico struct) e que unionacabam sendo propagados em todos os lugares, mesmo em bits específicos da arquitetura que poderia se importar menos com os outros.
Matthieu M.

0

Embora eu concorde que structs não devem ser usados ​​para acesso à porta de E / S de MCU, a pergunta original pode ser respondida desta forma:

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

Pode ser necessário substituir __attribute__((packed))por #pragma packou semelhante, dependendo da sintaxe do compilador.

A mistura de membros públicos e privados em uma estrutura normalmente resulta em que o layout de memória não seja mais garantido pelo padrão C ++. No entanto, se todos os membros não estáticos de uma estrutura forem privados, ela ainda será considerada POD / layout padrão e, portanto, as estruturas que os incorporam.

Por alguma razão, o gcc produz um aviso se um membro de uma estrutura anônima for privado, então eu tive que dar um nome a ele. Como alternativa, envolvê-lo em outra estrutura anônima também elimina o aviso (pode ser um bug).

Observe que o próprio spacermembro não é privado, portanto, os dados ainda podem ser acessados ​​desta forma:

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

No entanto, essa expressão parece um hack óbvio e, com sorte, não seria usada sem um bom motivo, muito menos como um erro.


1
Os usuários não podem declarar identificadores em nenhum namespace que contenha sublinhados duplos em qualquer lugar do nome (em C ++ ou apenas no início em C); fazer isso torna o código malformado. Esses nomes são reservados para a implementação e, portanto, podem em teoria entrar em conflito com os seus de maneiras terrivelmente sutis e caprichosas. De qualquer forma, o compilador não tem obrigação de reter seu código se ele os contiver. Esses nomes não são uma maneira rápida de obter nomes "internos" para seu próprio uso.
underscore_d

Obrigado, consertou.
Jack White

-1

Anti-solução.

NÃO FAÇA ISSO: Misture campos públicos e privados.

Talvez uma macro com um contador para gerar nomes de variáveis ​​únicos seja útil?

#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
Está bem. Se ninguém se importar, deixarei a resposta como o que não fazer.
Robert Andrzejuk

4
@NicHartley Considerando o número de respostas, estamos perto de uma questão de "pesquisa". Na pesquisa, o conhecimento dos impasses ainda é conhecimento, evita que outros sigam o caminho errado. 1 pela bravura.
Oliv

1
@Oliv E eu -1 porque o OP exigia algo, essa resposta violava o requisito e, portanto, é uma resposta ruim. Eu explicitamente não fiz nenhum julgamento de valor, positivo ou negativo, sobre a pessoa, em nenhum dos comentários - apenas sobre a resposta. Acho que podemos concordar que é ruim. O que isso diz sobre a pessoa está fora do assunto deste site. (Embora a IMO, qualquer pessoa disposta a dedicar algum tempo para contribuir com uma ideia esteja fazendo algo certo, mesmo que a ideia não dê certo )
Fund Monica's Lawsuit

2
Sim, é a resposta errada. Mas temo que algumas pessoas possam ter a mesma ideia. Por causa do comentário e do link acabei de aprender algo, isso não é um ponto negativo para mim.
Robert Andrzejuk
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.