¿Cómo creo un "espaciador" en una estructura de memoria de clase C ++?


94

La cuestión

En un contexto incrustado bare-metal de bajo nivel , me gustaría crear un espacio en blanco en la memoria, dentro de una estructura C ++ y sin ningún nombre, para prohibir al usuario acceder a dicha ubicación de memoria.

En este momento, lo he logrado poniendo un uint32_t :96;campo de bits feo que ocupará convenientemente el lugar de tres palabras, pero generará una advertencia de GCC (campo de bits demasiado grande para caber en uint32_t), lo cual es bastante legítimo.

Si bien funciona bien, no es muy limpio cuando desea distribuir una biblioteca con varios cientos de esas advertencias ...

¿Cómo lo hago correctamente?

¿Por qué hay un problema en primer lugar?

El proyecto en el que estoy trabajando consiste en definir la estructura de memoria de diferentes periféricos de toda una línea de microcontroladores (STMicroelectronics STM32). Para hacerlo, el resultado es una clase que contiene una unión de varias estructuras que definen todos los registros, dependiendo del microcontrolador objetivo.

Un ejemplo simple de un periférico bastante simple es el siguiente: una entrada / salida de uso general (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;
    };
};

Donde todo GPIO_MAPx_YYYes una macro, definida como uint32_t :32o el tipo de registro (una estructura dedicada).

Aquí ves el uint32_t :192;que funciona bien, pero activa una advertencia.

Lo que he considerado hasta ahora:

Podría haberlo reemplazado por varios uint32_t :32;(6 aquí), pero tengo algunos casos extremos en los que tengo uint32_t :1344;(42) (entre otros). Por lo tanto, preferiría no agregar unas cien líneas sobre otras 8k, aunque la generación de la estructura esté programada.

El mensaje de advertencia exacto es algo como: width of 'sool::ll::GPIO::<anonymous union>::<anonymous struct>::<anonymous>' exceeds its type(Me encanta lo sombrío que es).

Preferiría no resolver esto simplemente eliminando la advertencia, sino usando

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

puede ser una solución ... si la encuentro TheRightFlag. Sin embargo, como se señala en este hilo , gcc/cp/class.ccon esta triste parte del código:

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

Lo que nos dice que no hay una -Wxxxbandera para eliminar esta advertencia ...


26
¿has considerado char unused[12];y demás?
MM

3
Simplemente suprimiría la advertencia. [class.bit] / 1 garantiza el comportamiento de uint32_t :192;.
NathanOliver

3
@NathanOliver Yo también lo haría con mucho gusto, pero parece que esta advertencia no se puede suprimir (Usando GCC) o no encontré cómo hacerlo. Además, todavía no es una forma limpia de hacerlo (pero sería bastante satisfactorio). Me las arreglé para encontrar el indicador "-W" correcto, pero no pude aplicarlo solo en mis propios archivos (no quiero que el usuario elimine este tipo de advertencias por su trabajo)
J Faucher

3
Por cierto, puede escribir en :42*32lugar de:1344
MM

1
¿Intente esto para suprimir las advertencias? gcc.gnu.org/onlinedocs/gcc/…
Hitobat

Respuestas:


36

Utilice varios campos de bits anónimos adyacentes. Entonces en lugar de:

    uint32_t :160;

por ejemplo, tendrías:

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

Uno por cada registro que desee mantener en el anonimato.

Si tiene espacios grandes para llenar, puede ser más claro y menos propenso a errores usar macros para repetir el espacio único de 32 bits. Por ejemplo, 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)

Luego, se puede agregar un espacio de 1344 (42 * 32 bits) así:

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

Gracias por la respuesta. Ya lo consideré, sin embargo, agregaría más de 200 líneas en algunos de mis archivos ( uint32_t :1344;está en el lugar), así que preferiría no tener que ir por este camino ...
J Faucher

1
@JFaucher Agregó una posible solución a su requisito de recuento de líneas. Si tiene tales requisitos, puede mencionarlos en la pregunta para evitar obtener respuestas que no los cumplan.
Clifford

Gracias por la edición y perdón por no mencionar el recuento de líneas. Mi punto es que mi código ya es doloroso para sumergirse ya que hay muchas líneas y prefiero evitar agregar muchas más. Por lo tanto, estaba preguntando si alguien conocía una forma "limpia" u "oficial" de evitar el uso de campos de bits anónimos adyacentes (incluso si eso funciona bien). Sin embargo, el enfoque macro me parece bien. Por cierto, en tu ejemplo, ¿no tienes un espacio de 36 * 32 bits?
J Faucher

@JFaucher - corregido. Los archivos de mapeo de registros de E / S son necesariamente grandes debido a la gran cantidad de registros; normalmente se escribe una vez y el mantenimiento no es un problema porque el hardware es una constante. Excepto al "ocultar" los registros, usted está haciendo el trabajo de mantenimiento por sí mismo si luego necesita acceder a ellos. ¿Sabe, por supuesto, que todos los dispositivos STM32 ya tienen un encabezado de mapa de registro proporcionado por el proveedor? Sería mucho menos propenso a errores usar eso.
Clifford

2
Estoy de acuerdo con usted y, para ser justos, creo que seguiré uno de los dos métodos que se muestran en su respuesta. Solo quería estar seguro de que C ++ no proporciona una mejor solución antes de hacerlo. Soy muy consciente de que ST proporciona esos encabezados, sin embargo, se basan en el uso masivo de macros y operaciones bit a bit. Mi proyecto es construir un C ++ equivalente a esos encabezados que serán menos propensos a errores (usando clases de enumeración, campos de bits, etc.). Es por eso que usamos un script para "traducir" los encabezados CMSIS en nuestras estructuras C ++ (y encontramos algunos errores en los archivos ST por cierto)
J Faucher

45

¿Qué tal una forma de 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;
}

Obtienes autocompletado debido al GPIOespacio de nombres y no hay necesidad de relleno ficticio. Incluso, está más claro lo que está sucediendo, ya que puede ver la dirección de cada registro, no tiene que depender en absoluto del comportamiento de relleno del compilador.


1
Es posible que esto se optimice menos que una estructura para acceder a múltiples registros MMIO desde la misma función. Con un puntero a la dirección base en un registro, el compilador puede usar instrucciones de carga / almacenamiento con desplazamientos inmediatos, como ldr r0, [r4, #16], mientras que es más probable que los compiladores pierdan esa optimización con cada dirección declarada por separado. GCC probablemente cargará cada dirección GPIO en un registro separado. (De un grupo literal, aunque algunos de ellos se pueden representar como inmediatos rotados en codificación Thumb.)
Peter Cordes

4
Resulta que mis preocupaciones eran infundadas; ARM GCC también se optimiza de esta manera. godbolt.org/z/ztB7hi . Pero tenga en cuenta que quiere static volatile uint32_t &MAP0_MODER, no inline. Una inlinevariable no se compila. ( staticevita tener almacenamiento estático para el puntero, y volatilees exactamente lo que desea para MMIO para evitar la eliminación de almacenamiento muerto o la optimización de escritura / lectura)
Peter Cordes

1
@PeterCordes: variables en línea es una nueva característica de C ++ 17. Pero tienes razón, lo statichace igualmente bien en este caso. Gracias por mencionarlo volatile, lo agregaré a mi respuesta (y cambiaré en línea a estático, por lo que funciona para versiones anteriores de C ++ 17).
geza

2
Este no es un comportamiento estrictamente bien definido, vea este hilo de Twitter y tal vez este sea útil
Shafik Yaghmour

1
@JFaucher: cree tantos espacios de nombres como estructuras tenga y use funciones independientes en ese espacio de nombres. Entonces, lo tendrás GPIOA::togglePin().
geza

20

En el ámbito de los sistemas integrados, puede modelar hardware mediante el uso de una estructura o la definición de punteros a las direcciones de registro.

No se recomienda el modelado por estructura porque el compilador puede agregar relleno entre los miembros con fines de alineación (aunque muchos compiladores de sistemas integrados tienen un pragma para empaquetar la estructura).

Ejemplo:

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

También puede usar la notación de matriz:

uint16_t status = UART1[UART_STATUS_OFFSET];  

Si debe usar la estructura, en mi humilde opinión, el mejor método para omitir direcciones sería definir un miembro y no acceder a él:

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

En uno de nuestros proyectos tenemos tanto constantes como estructuras de diferentes proveedores (el proveedor 1 usa constantes mientras que el proveedor 2 usa estructuras).


Gracias por tu respuesta. Sin embargo, elijo usar un enfoque de estructura para facilitar el trabajo del usuario cuando obtiene una función de autocompletar (solo se mostrarán los atributos correctos) y no quiero "mostrar" al usuario los espacios reservados como señaló en un comentario de mi primera publicación.
J Faucher

Aún puede tener eso haciendo que la dirección anterior sea staticmiembro de una estructura, asumiendo que la función de autocompletar puede mostrar miembros estáticos. De lo contrario, también pueden ser funciones miembro en línea.
Phil1970

@JFaucher No soy una persona de sistemas integrados y no he probado esto, pero ¿no se resolvería el problema de autocompletar declarando privado al miembro reservado? (Puede declarar miembros privados en una estructura y puede usar public:y private:tantas veces como desee para obtener el orden correcto de los campos).
Nathaniel

1
@Nathaniel: No es así; si una clase tiene miembros de datos no estáticos publicy privateno estáticos, entonces no es un tipo de diseño estándar , por lo que no proporciona las garantías de ordenación que está pensando. (Y estoy bastante seguro de que el caso de uso del OP requiere un tipo de diseño estándar).
ruakh

1
No olvide volatileesas declaraciones, por cierto, para registros de E / S asignados en memoria.
Peter Cordes

13

geza tiene razón en que realmente no quieres usar clases para esto.

Pero, si insistiera, la mejor manera de agregar un miembro no utilizado de n bytes de ancho es simplemente hacerlo:

char unused[n];

Si agrega un pragma específico de implementación para evitar la adición de relleno arbitrario a los miembros de la clase, esto puede funcionar.


Para GNU C / C ++ (gcc, clang y otros que admiten las mismas extensiones), uno de los lugares válidos para poner el atributo es:

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

(ejemplo en el explorador del compilador Godbolt que muestra offsetof(GPIO, b)= 7 bytes).


9

Para ampliar las respuestas de @ Clifford y @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;
}

He incorporado una variante de su sugerencia en mi respuesta siguiendo requisitos adicionales en un comentario. Crédito a quien crédito merece.
Clifford

7

Para ampliar la respuesta de Clifford, siempre puede realizar una macro de los campos de bits anónimos.

Entonces en lugar de

uint32_t :160;

utilizar

#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

Y luego úsalo como

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

Desafortunadamente, necesitará tantas EMPTY_32_Xvariantes como bytes tenga :( Aún así, le permite tener declaraciones únicas en su estructura.


5
Con las macros de Boost CPP, creo que puede utilizar la recursividad para evitar tener que crear manualmente todas las macros necesarias.
Peter Cordes

3
Puede ponerlos en cascada (hasta el límite de recursividad del preprocesador, pero eso suele ser suficiente). Así #define EMPTY_32_2 EMPTY_32_1; EMPTY_32_1y #define EMPTY_32_3 EMPTY_32_2; EMPTY_32_1etc.
Miral

@PeterCordes quizás, pero las etiquetas indican que quizás se requiera compatibilidad con C y C ++.
Clifford

2
C y C ++ utilizan el mismo preprocesador de C; No veo otro problema que no sea tal vez hacer que el encabezado de impulso necesario esté disponible para C. Ellos ponen las cosas de CPP-macro en un encabezado separado.
Peter Cordes

1

Para definir un espaciador grande 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

Creo que sería beneficioso introducir más estructura; lo que, a su vez, puede resolver el problema de los espaciadores.

Nombra las variantes

Si bien los espacios de nombres planos son agradables, el problema es que termina con una colección heterogénea de campos y no hay una forma sencilla de pasar todos los campos relacionados juntos. Además, al usar estructuras anónimas en una unión anónima, no puede pasar referencias a las estructuras en sí ni usarlas como parámetros de plantilla.

Como primer paso, consideraría, por lo tanto, romper elstruct :

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

Y finalmente, el encabezado global:

// Gpio.h
#pragma once

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

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

Ahora, puedo escribir un void special_map0(Gpio:: Map0 volatile& map);, así como obtener una descripción general rápida de todas las arquitecturas disponibles de un vistazo.

Espaciadores simples

Con la definición dividida en varios encabezados, los encabezados son individualmente mucho más manejables.

Por lo tanto, mi enfoque inicial para cumplir exactamente con sus requisitos sería seguir repitiendo std::uint32_t:32;. Sí, agrega algunas líneas de centenas a las líneas de 8k existentes, pero dado que cada encabezado es individualmente más pequeño, puede que no sea tan malo.

Sin embargo, si está dispuesto a considerar soluciones más exóticas ...

Presentamos $.

Es un hecho poco conocido que $es un carácter viable para los identificadores de C ++; incluso es un carácter inicial viable (a diferencia de los dígitos).

Una $aparición en el código fuente probablemente $$$$llamaría la atención y definitivamente llamará la atención durante las revisiones del código. Esto es algo que puede aprovechar fácilmente:

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

Incluso puede armar un simple "lint" como un gancho de confirmación previa o en su CI que busca $$$$en el código C ++ comprometido y rechaza dichas confirmaciones.


1
Recuerde que el caso de uso específico del OP es para describir registros de E / S asignados en memoria al compilador. Que no tiene sentido copiar toda la estructura por valor. (Y GPIO_MAP0_MODERes de suponer que cada miembro me gusta volatile.) Sin embargo, posiblemente el uso de referencia o parámetro de plantilla de los miembros previamente anónimos podría ser útil. Y para el caso general de estructuras de relleno, claro. Pero el caso de uso explica por qué el OP los dejó en el anonimato.
Peter Cordes

Puede usar $$$padding##Index_[N_];para hacer que el nombre del campo sea más autoexplicativo en caso de que alguna vez aparezca en autocompletar o al depurar. (O zz$$$paddingpara ordenarlo después de los GPIO...nombres, porque el objetivo de este ejercicio de acuerdo con el OP es autocompletar mejor para los nombres de ubicación de E / S mapeados en memoria)
Peter Cordes

@PeterCordes: volví a escanear la respuesta para comprobarlo y nunca vi ninguna mención de copia. Sin embargo, olvidé el volatilecalificativo de la referencia, que se ha corregido. En cuanto a nombrar; Lo dejaré hasta el OP. Hay muchas variaciones (relleno, reservado, ...), e incluso el "mejor" prefijo para la finalización automática puede depender del IDE en cuestión, aunque aprecio la idea de modificar la clasificación.
Matthieu M.

Me refería a " y no hay una forma sencilla de pasar todos los campos relacionados juntos ", lo que suena como asignación de estructura, y el resto de la oración sobre nombrar los miembros de estructura de la unión.
Peter Cordes

1
@PeterCordes: Estaba pensando en pasar por referencia, como se ilustra más adelante. Me parece incómodo que la estructura del OP les impida crear "módulos" que se puede probar estáticamente para acceder solo a una arquitectura específica (tomando una referencia a la específica struct) y que unionterminan propagándose por todas partes incluso en bits específicos de la arquitectura que podría importarle menos los demás.
Matthieu M.

0

Aunque acepto que las estructuras no deben usarse para el acceso al puerto de E / S de MCU, la pregunta original se puede responder de esta manera:

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

Es posible que deba reemplazar __attribute__((packed))con #pragma packo similar según la sintaxis de su compilador.

La combinación de miembros públicos y privados en una estructura normalmente da como resultado que el diseño de la memoria ya no está garantizado por el estándar C ++. Sin embargo, si todos los miembros no estáticos de una estructura son privados, todavía se considera POD / diseño estándar, y también lo son las estructuras que los incrustan.

Por alguna razón, gcc produce una advertencia si un miembro de una estructura anónima es privado, así que tuve que darle un nombre. Alternativamente, envolverlo en otra estructura anónima también elimina la advertencia (esto puede ser un error).

Tenga en cuenta que el spacermiembro no es en sí mismo privado, por lo que aún se puede acceder a los datos de esta manera:

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

Sin embargo, tal expresión parece un truco obvio y, con suerte, no se usaría sin una buena razón, y mucho menos como un error.


1
Los usuarios no pueden declarar identificadores en ningún espacio de nombres que contenga guiones bajos dobles en cualquier parte del nombre (en C ++, o solo al principio en C); al hacerlo, el código está mal formado. Estos nombres están reservados para la implementación y, por lo tanto, en teoría, pueden entrar en conflicto con los suyos de formas horriblemente sutiles y caprichosas. De todos modos, el compilador no tiene la obligación de retener su código si lo contiene. Estos nombres no son una forma rápida de obtener nombres "internos" para su propio uso.
underscore_d

Gracias, lo arreglé.
Jack White

-1

Anti-solución.

NO HAGA ESTO: Mezcle los campos público y privado.

¿Quizás sea útil una macro con un contador para generar nombres de variables únicos?

#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
Okay. Si a nadie le importa, dejaré la respuesta como qué no hacer.
Robert Andrzejuk

4
@NicHartley Considerando el número de respuestas, estamos cerca de una pregunta de "investigación". En la investigación, el conocimiento de los callejones sin salida sigue siendo conocimiento, evita que otros tomen el camino equivocado. +1 por la valentía.
Oliv

1
@Oliv Y yo hice un -1 porque el OP requería algo, esta respuesta violó el requisito y, por lo tanto, es una mala respuesta. Explícitamente no hice ningún juicio de valor, positivo o negativo, sobre la persona, en ninguno de los comentarios, solo en la respuesta. Creo que ambos podemos estar de acuerdo en que es malo. Lo que eso dice sobre la persona está fuera de tema para este sitio. (Aunque, en mi opinión, cualquiera que esté dispuesto a tomarse un tiempo para contribuir con una idea está haciendo algo bien, incluso si la idea no funciona)
Financia la demanda de Monica

2
Sí, es la respuesta incorrecta. Pero me temo que algunas personas pueden llegar a la misma idea. Por el comentario y el enlace acabo de aprender algo, eso no es puntos negativos para mí.
Robert Andrzejuk
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.